negotium 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/chunk-0ay6cjdy.js +1331 -0
- package/dist/chunk-0ay6cjdy.js.map +16 -0
- package/dist/chunk-1k7t7bnx.js +97 -0
- package/dist/chunk-1k7t7bnx.js.map +10 -0
- package/dist/chunk-5ahkew9c.js +806 -0
- package/dist/chunk-5ahkew9c.js.map +17 -0
- package/dist/chunk-6cpn7ht2.js +287 -0
- package/dist/chunk-6cpn7ht2.js.map +13 -0
- package/dist/chunk-6kctn20e.js +3529 -0
- package/dist/chunk-6kctn20e.js.map +20 -0
- package/dist/chunk-7xqj7aex.js +77 -0
- package/dist/chunk-7xqj7aex.js.map +11 -0
- package/dist/chunk-9p0ww3tk.js +41 -0
- package/dist/chunk-9p0ww3tk.js.map +10 -0
- package/dist/chunk-a92a7nrk.js +252 -0
- package/dist/chunk-a92a7nrk.js.map +10 -0
- package/dist/chunk-c500xe9z.js +33 -0
- package/dist/chunk-c500xe9z.js.map +10 -0
- package/dist/chunk-cds1qxsf.js +1644 -0
- package/dist/chunk-cds1qxsf.js.map +17 -0
- package/dist/chunk-e4407g8x.js +274 -0
- package/dist/chunk-e4407g8x.js.map +10 -0
- package/dist/chunk-e5pbggsh.js +363 -0
- package/dist/chunk-e5pbggsh.js.map +11 -0
- package/dist/chunk-edb07sjp.js +567 -0
- package/dist/chunk-edb07sjp.js.map +14 -0
- package/dist/chunk-exd0v74a.js +3012 -0
- package/dist/chunk-exd0v74a.js.map +32 -0
- package/dist/chunk-fgrme9tr.js +4398 -0
- package/dist/chunk-fgrme9tr.js.map +47 -0
- package/dist/chunk-fka9eqe1.js +581 -0
- package/dist/chunk-fka9eqe1.js.map +11 -0
- package/dist/chunk-g82jygez.js +108 -0
- package/dist/chunk-g82jygez.js.map +10 -0
- package/dist/chunk-ghs2j1fr.js +312 -0
- package/dist/chunk-ghs2j1fr.js.map +10 -0
- package/dist/chunk-gmy11efb.js +125 -0
- package/dist/chunk-gmy11efb.js.map +10 -0
- package/dist/chunk-gxma4c70.js +110 -0
- package/dist/chunk-gxma4c70.js.map +10 -0
- package/dist/chunk-hkdqhm34.js +61 -0
- package/dist/chunk-hkdqhm34.js.map +10 -0
- package/dist/chunk-hpa0cpzh.js +134 -0
- package/dist/chunk-hpa0cpzh.js.map +10 -0
- package/dist/chunk-hwy3n31w.js +21 -0
- package/dist/chunk-hwy3n31w.js.map +9 -0
- package/dist/chunk-j2behpy7.js +214 -0
- package/dist/chunk-j2behpy7.js.map +10 -0
- package/dist/chunk-jc0at0tc.js +102 -0
- package/dist/chunk-jc0at0tc.js.map +10 -0
- package/dist/chunk-jkqq5ef6.js +243 -0
- package/dist/chunk-jkqq5ef6.js.map +11 -0
- package/dist/chunk-jzkw2f01.js +51 -0
- package/dist/chunk-jzkw2f01.js.map +10 -0
- package/dist/chunk-jzz91jwh.js +70 -0
- package/dist/chunk-jzz91jwh.js.map +10 -0
- package/dist/chunk-k9z47mab.js +247 -0
- package/dist/chunk-k9z47mab.js.map +12 -0
- package/dist/chunk-n17yxpa8.js +23 -0
- package/dist/chunk-n17yxpa8.js.map +10 -0
- package/dist/chunk-naa09ags.js +2143 -0
- package/dist/chunk-naa09ags.js.map +15 -0
- package/dist/chunk-ns3ke88n.js +142 -0
- package/dist/chunk-ns3ke88n.js.map +10 -0
- package/dist/chunk-p3cr2ejg.js +1626 -0
- package/dist/chunk-p3cr2ejg.js.map +15 -0
- package/dist/chunk-q5bcpf23.js +2925 -0
- package/dist/chunk-q5bcpf23.js.map +20 -0
- package/dist/chunk-rafc55bs.js +424 -0
- package/dist/chunk-rafc55bs.js.map +11 -0
- package/dist/chunk-rbrxjvbj.js +2830 -0
- package/dist/chunk-rbrxjvbj.js.map +23 -0
- package/dist/chunk-sa874j5c.js +9 -0
- package/dist/chunk-sa874j5c.js.map +10 -0
- package/dist/chunk-sgsz2fpq.js +289 -0
- package/dist/chunk-sgsz2fpq.js.map +10 -0
- package/dist/chunk-sm6fh5c5.js +569 -0
- package/dist/chunk-sm6fh5c5.js.map +14 -0
- package/dist/chunk-t9zcg8f1.js +499 -0
- package/dist/chunk-t9zcg8f1.js.map +13 -0
- package/dist/chunk-vp2db42d.js +121 -0
- package/dist/chunk-vp2db42d.js.map +10 -0
- package/dist/chunk-w13jjf4a.js +46 -0
- package/dist/chunk-w13jjf4a.js.map +10 -0
- package/dist/chunk-x1arr7ze.js +63 -0
- package/dist/chunk-x1arr7ze.js.map +10 -0
- package/dist/chunk-xfq68rch.js +583 -0
- package/dist/chunk-xfq68rch.js.map +11 -0
- package/dist/main.js +132 -20
- package/dist/main.js.map +4 -4
- package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
- package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
- package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
- package/dist/runtime/src/agents/archiver.ts +396 -0
- package/dist/runtime/src/agents/auth-check.ts +64 -0
- package/dist/runtime/src/agents/claude-provider.ts +737 -0
- package/dist/runtime/src/agents/claude-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
- package/dist/runtime/src/agents/codex-provider.ts +785 -0
- package/dist/runtime/src/agents/codex-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
- package/dist/runtime/src/agents/contracts.ts +69 -0
- package/dist/runtime/src/agents/deep-map.ts +13 -0
- package/dist/runtime/src/agents/execution-host.ts +91 -0
- package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
- package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
- package/dist/runtime/src/agents/fork.ts +65 -0
- package/dist/runtime/src/agents/hosted-agent.ts +48 -0
- package/dist/runtime/src/agents/idle-archiver.ts +128 -0
- package/dist/runtime/src/agents/index.ts +205 -0
- package/dist/runtime/src/agents/maestro-provider.ts +206 -0
- package/dist/runtime/src/agents/maestro-registry.ts +31 -0
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
- package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
- package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
- package/dist/runtime/src/agents/model-catalog.ts +151 -0
- package/dist/runtime/src/agents/registry.ts +24 -0
- package/dist/runtime/src/agents/rollout/claude.ts +331 -0
- package/dist/runtime/src/agents/rollout/codex.ts +569 -0
- package/dist/runtime/src/agents/rollout/shared.ts +205 -0
- package/dist/runtime/src/agents/self-config-core.ts +415 -0
- package/dist/runtime/src/agents/task-events.ts +44 -0
- package/dist/runtime/src/agents/tool-format.ts +352 -0
- package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
- package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
- package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
- package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
- package/dist/runtime/src/application/submit-user-message.ts +47 -0
- package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
- package/dist/runtime/src/application/switch-topic-model.ts +79 -0
- package/dist/runtime/src/application/topic-service.ts +139 -0
- package/dist/runtime/src/application/vault-command.ts +170 -0
- package/dist/runtime/src/bus.ts +319 -0
- package/dist/runtime/src/index.ts +333 -0
- package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
- package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
- package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
- package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
- package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
- package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
- package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +134 -0
- package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
- package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
- package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
- package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
- package/dist/runtime/src/mcp/system-health-server.ts +130 -0
- package/dist/runtime/src/mcp/task-server.ts +173 -0
- package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
- package/dist/runtime/src/mcp/vault-http.ts +219 -0
- package/dist/runtime/src/mcp/vault-run.ts +153 -0
- package/dist/runtime/src/mcp/vault-server.ts +104 -0
- package/dist/runtime/src/mcp/wiki-server.ts +755 -0
- package/dist/runtime/src/media/file-events.ts +33 -0
- package/dist/runtime/src/media/text-extractor.ts +312 -0
- package/dist/runtime/src/media/video.ts +92 -0
- package/dist/runtime/src/outbox/file-ops.ts +210 -0
- package/dist/runtime/src/outbox/index.ts +8 -0
- package/dist/runtime/src/outbox/utils.ts +51 -0
- package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
- package/dist/runtime/src/platform/config.ts +371 -0
- package/dist/runtime/src/platform/constants.ts +18 -0
- package/dist/runtime/src/platform/debug-users.ts +14 -0
- package/dist/runtime/src/platform/delay.ts +1 -0
- package/dist/runtime/src/platform/error.ts +9 -0
- package/dist/runtime/src/platform/file-utils.ts +16 -0
- package/dist/runtime/src/platform/jsonl.ts +202 -0
- package/dist/runtime/src/platform/lifecycle.ts +174 -0
- package/dist/runtime/src/platform/logger.ts +31 -0
- package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
- package/dist/runtime/src/platform/mcp-config.ts +865 -0
- package/dist/runtime/src/platform/modules.ts +145 -0
- package/dist/runtime/src/platform/node-plugins.ts +46 -0
- package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
- package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
- package/dist/runtime/src/prompts/builders.ts +357 -0
- package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
- package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
- package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
- package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
- package/dist/runtime/src/query/active-rooms.ts +532 -0
- package/dist/runtime/src/query/control.ts +313 -0
- package/dist/runtime/src/query/index.ts +24 -0
- package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
- package/dist/runtime/src/query/session-inbox-path.ts +42 -0
- package/dist/runtime/src/query/state.ts +94 -0
- package/dist/runtime/src/query/types.ts +83 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
- package/dist/runtime/src/runtime/attachments.ts +151 -0
- package/dist/runtime/src/runtime/background-sessions.ts +83 -0
- package/dist/runtime/src/runtime/channel-context.ts +107 -0
- package/dist/runtime/src/runtime/errors.ts +69 -0
- package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
- package/dist/runtime/src/runtime/file-hooks.ts +81 -0
- package/dist/runtime/src/runtime/footer.ts +23 -0
- package/dist/runtime/src/runtime/inbox.ts +946 -0
- package/dist/runtime/src/runtime/tasks.ts +56 -0
- package/dist/runtime/src/runtime/topic-config.ts +29 -0
- package/dist/runtime/src/runtime/turn-runner.ts +2286 -0
- package/dist/runtime/src/runtime/usage-alert.ts +84 -0
- package/dist/runtime/src/runtime/visual-html.ts +109 -0
- package/dist/runtime/src/runtime/visual-store.ts +430 -0
- package/dist/runtime/src/runtime/visuals.ts +171 -0
- package/dist/runtime/src/security/sanitize.ts +27 -0
- package/dist/runtime/src/security/sensitive-path.ts +34 -0
- package/dist/runtime/src/storage/activity-log.ts +152 -0
- package/dist/runtime/src/storage/api-messages.ts +617 -0
- package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
- package/dist/runtime/src/storage/api-topic-config.ts +192 -0
- package/dist/runtime/src/storage/api-topics.ts +844 -0
- package/dist/runtime/src/storage/app-settings.ts +44 -0
- package/dist/runtime/src/storage/browser-profiles.ts +133 -0
- package/dist/runtime/src/storage/conversations.ts +268 -0
- package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
- package/dist/runtime/src/storage/forum/index.ts +4 -0
- package/dist/runtime/src/storage/forum/repository.ts +283 -0
- package/dist/runtime/src/storage/forum/schema.ts +300 -0
- package/dist/runtime/src/storage/forum-db.ts +23 -0
- package/dist/runtime/src/storage/runtime-events.ts +130 -0
- package/dist/runtime/src/storage/runtime-leases.ts +189 -0
- package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
- package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +300 -0
- package/dist/runtime/src/storage/self-schedules.ts +298 -0
- package/dist/runtime/src/storage/session-asks.ts +347 -0
- package/dist/runtime/src/storage/sqlite.ts +129 -0
- package/dist/runtime/src/storage/tasks.ts +179 -0
- package/dist/runtime/src/storage/token-stats.ts +144 -0
- package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
- package/dist/runtime/src/storage/topic-archive.ts +74 -0
- package/dist/runtime/src/storage/topic-settings.ts +67 -0
- package/dist/runtime/src/storage/topic-transcript.ts +98 -0
- package/dist/runtime/src/storage/vault-crypto.ts +65 -0
- package/dist/runtime/src/storage/vault.ts +225 -0
- package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
- package/dist/runtime/src/storage/wiki.ts +85 -0
- package/dist/runtime/src/topics/create.ts +123 -0
- package/dist/runtime/src/topics/derive.ts +398 -0
- package/dist/runtime/src/topics/lifecycle.ts +311 -0
- package/dist/runtime/src/topics/links.ts +7 -0
- package/dist/runtime/src/topics/personal-general.ts +65 -0
- package/dist/runtime/src/topics/session.ts +381 -0
- package/dist/runtime/src/types/api.ts +497 -0
- package/dist/runtime/src/types.ts +255 -0
- package/dist/runtime/src/version.ts +1 -0
- package/dist/runtime/tsconfig.json +13 -0
- package/package.json +16 -3
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
// spawn_subagent runtime MCP tool — agent-initiated delegation to a child
|
|
2
|
+
// agent room (clawgram parity). The tool returns immediately (fire-and-forget):
|
|
3
|
+
// the child topic runs its own fresh session, a live card message in the parent
|
|
4
|
+
// room tracks its lifecycle, and the child's final response is injected back
|
|
5
|
+
// into the parent as an AI turn on completion.
|
|
6
|
+
//
|
|
7
|
+
// Watches are in-memory (same accepted tradeoff as the ask-callback registry in
|
|
8
|
+
// routes/sessions.ts); `sweepStaleSubagentCards` marks orphans failed on boot.
|
|
9
|
+
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { errorResult, type SharedMcpTool, textResult } from "#agents/mcp-tools/common";
|
|
12
|
+
import { WsHub } from "#bus";
|
|
13
|
+
import { logger } from "#platform/logger";
|
|
14
|
+
import {
|
|
15
|
+
appendApiMessage,
|
|
16
|
+
getApiMessage,
|
|
17
|
+
listApiMessagesByKind,
|
|
18
|
+
updateApiMessageSubagentCard,
|
|
19
|
+
} from "#storage/api-messages";
|
|
20
|
+
import { getTopic, listTopics } from "#storage/api-topics";
|
|
21
|
+
import { type AgentKind, isAgentKind } from "#types";
|
|
22
|
+
import type { MessageDto, SubagentCardDto } from "#types/api";
|
|
23
|
+
|
|
24
|
+
const MAX_TASK_CHARS = 8000;
|
|
25
|
+
const MAX_NAME_CHARS = 80;
|
|
26
|
+
const MAX_LIVE_CHILDREN_PER_PARENT = 5;
|
|
27
|
+
const RESULT_SUMMARY_CHARS = 300;
|
|
28
|
+
|
|
29
|
+
export interface SpawnSubagentToolContext {
|
|
30
|
+
userId: string;
|
|
31
|
+
topicId: string;
|
|
32
|
+
queryId?: string;
|
|
33
|
+
agent: AgentKind;
|
|
34
|
+
model?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type SubagentToolContext = Pick<SpawnSubagentToolContext, "userId" | "topicId">;
|
|
38
|
+
|
|
39
|
+
export interface SubagentWatch {
|
|
40
|
+
parentTopicId: string;
|
|
41
|
+
childTopicId: string;
|
|
42
|
+
cardMessageId: string;
|
|
43
|
+
name: string;
|
|
44
|
+
userId: string;
|
|
45
|
+
startedAt: string;
|
|
46
|
+
queryId?: string;
|
|
47
|
+
running: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const watchesByChild = new Map<string, SubagentWatch>();
|
|
51
|
+
const childByQueryId = new Map<string, string>();
|
|
52
|
+
|
|
53
|
+
function countLiveChildren(parentTopicId: string): number {
|
|
54
|
+
let n = 0;
|
|
55
|
+
for (const watch of watchesByChild.values()) {
|
|
56
|
+
if (watch.parentTopicId === parentTopicId) n += 1;
|
|
57
|
+
}
|
|
58
|
+
return n;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Read-merge-write the parent card and broadcast the patch. */
|
|
62
|
+
function patchSubagentCard(
|
|
63
|
+
parentTopicId: string,
|
|
64
|
+
cardMessageId: string,
|
|
65
|
+
patch: Partial<SubagentCardDto>,
|
|
66
|
+
): void {
|
|
67
|
+
const current = getApiMessage(parentTopicId, cardMessageId)?.subagentCard;
|
|
68
|
+
if (!current) return;
|
|
69
|
+
const next: SubagentCardDto = { ...current, ...patch };
|
|
70
|
+
const editedAt = new Date().toISOString();
|
|
71
|
+
if (!updateApiMessageSubagentCard(parentTopicId, cardMessageId, next, editedAt)) return;
|
|
72
|
+
WsHub.get().broadcastMessageUpdated(parentTopicId, cardMessageId, {
|
|
73
|
+
subagentCard: next,
|
|
74
|
+
editedAt,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function registerWatchDispatch(watch: SubagentWatch, queryId: string): void {
|
|
79
|
+
// Re-dispatch (defer drain, session-expired retry, supersede-requeue) hands
|
|
80
|
+
// the same child a new queryId — drop the stale mapping first.
|
|
81
|
+
if (watch.queryId) childByQueryId.delete(watch.queryId);
|
|
82
|
+
watch.queryId = queryId;
|
|
83
|
+
watchesByChild.set(watch.childTopicId, watch);
|
|
84
|
+
childByQueryId.set(queryId, watch.childTopicId);
|
|
85
|
+
if (!watch.running) {
|
|
86
|
+
watch.running = true;
|
|
87
|
+
patchSubagentCard(watch.parentTopicId, watch.cardMessageId, { status: "running" });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Claim the watch for a finished child turn (delete-on-take). */
|
|
92
|
+
export function takeSubagentWatch(queryId: string): SubagentWatch | null {
|
|
93
|
+
const childTopicId = childByQueryId.get(queryId);
|
|
94
|
+
if (!childTopicId) return null;
|
|
95
|
+
childByQueryId.delete(queryId);
|
|
96
|
+
const watch = watchesByChild.get(childTopicId);
|
|
97
|
+
if (!watch || watch.queryId !== queryId) return null;
|
|
98
|
+
watchesByChild.delete(childTopicId);
|
|
99
|
+
return watch;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function dropWatch(watch: SubagentWatch): void {
|
|
103
|
+
watchesByChild.delete(watch.childTopicId);
|
|
104
|
+
if (watch.queryId) childByQueryId.delete(watch.queryId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Drop deferred/in-flight bookkeeping when a subagent room is hard-deleted. */
|
|
108
|
+
export function cancelSubagentWatchForDeletedTopic(childTopicId: string): void {
|
|
109
|
+
const watch = watchesByChild.get(childTopicId);
|
|
110
|
+
if (!watch) return;
|
|
111
|
+
dropWatch(watch);
|
|
112
|
+
patchSubagentCard(watch.parentTopicId, watch.cardMessageId, {
|
|
113
|
+
status: "failed",
|
|
114
|
+
errorMessage: "subagent room was deleted",
|
|
115
|
+
finishedAt: new Date().toISOString(),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Last-resort delivery: a plain system message in the parent room. */
|
|
120
|
+
function appendParentSystemNote(watch: SubagentWatch, prompt: string): void {
|
|
121
|
+
const now = new Date().toISOString();
|
|
122
|
+
const msg: MessageDto = {
|
|
123
|
+
id: `subagent-note-${watch.childTopicId}-${now}`,
|
|
124
|
+
topicId: watch.parentTopicId,
|
|
125
|
+
authorId: "system",
|
|
126
|
+
text: prompt,
|
|
127
|
+
kind: "system",
|
|
128
|
+
createdAt: now,
|
|
129
|
+
};
|
|
130
|
+
appendApiMessage(msg);
|
|
131
|
+
WsHub.get().broadcastMessage(watch.parentTopicId, msg);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Deliver a completion/failure notice to the parent room as a hidden AI turn,
|
|
136
|
+
* falling back to a system message. NEVER throws — the caller has already
|
|
137
|
+
* taken the watch, so a lost exception here would silently swallow the result
|
|
138
|
+
* with nothing left to retry.
|
|
139
|
+
*/
|
|
140
|
+
async function notifyParent(
|
|
141
|
+
watch: SubagentWatch,
|
|
142
|
+
prompt: string,
|
|
143
|
+
noteKind: "done" | "fail",
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
try {
|
|
146
|
+
const parent = getTopic(watch.parentTopicId);
|
|
147
|
+
if (!parent) return;
|
|
148
|
+
const requestId = `subagent-${noteKind}-${watch.childTopicId}`;
|
|
149
|
+
// A stable requestId is required for the defer queue: injects without one are
|
|
150
|
+
// DROPPED when the parent room is busy (InterSessionQueue.enqueue). One
|
|
151
|
+
// settle per child, so the id also dedups accidental double-delivery.
|
|
152
|
+
const { triggerTopicAiTurn } = await import("#runtime/turn-runner");
|
|
153
|
+
const queryId = parent.agent
|
|
154
|
+
? triggerTopicAiTurn(watch.parentTopicId, watch.userId, prompt, undefined, {
|
|
155
|
+
origin: `subagent:${watch.name}`,
|
|
156
|
+
requestId,
|
|
157
|
+
hideInjectMessage: true,
|
|
158
|
+
})
|
|
159
|
+
: null;
|
|
160
|
+
if (queryId) return;
|
|
161
|
+
|
|
162
|
+
// null return = deferred behind a running parent turn (it will drain) — but
|
|
163
|
+
// only when the queue actually accepted it. Anything else falls back to a
|
|
164
|
+
// plain system message so the result is never lost.
|
|
165
|
+
const { interSessionQueue } = await import("#query/active-rooms");
|
|
166
|
+
if (parent.agent && interSessionQueue.hasRequest(watch.parentTopicId, requestId)) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
appendParentSystemNote(watch, prompt);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
logger.warn(
|
|
173
|
+
{ err, parentTopicId: watch.parentTopicId, childTopicId: watch.childTopicId, noteKind },
|
|
174
|
+
"subagent: parent AI notify failed — falling back to system message",
|
|
175
|
+
);
|
|
176
|
+
try {
|
|
177
|
+
appendParentSystemNote(watch, prompt);
|
|
178
|
+
} catch (fallbackErr) {
|
|
179
|
+
logger.error(
|
|
180
|
+
{ err: fallbackErr, parentTopicId: watch.parentTopicId, childTopicId: watch.childTopicId },
|
|
181
|
+
"subagent: parent delivery lost — system-message fallback also failed",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Settle a finished child run. NEVER rejects — call sites fire-and-forget. */
|
|
188
|
+
export async function settleSubagentSuccess(
|
|
189
|
+
watch: SubagentWatch,
|
|
190
|
+
finalText: string,
|
|
191
|
+
): Promise<void> {
|
|
192
|
+
const summary = finalText.replace(/\s+/g, " ").trim().slice(0, RESULT_SUMMARY_CHARS);
|
|
193
|
+
try {
|
|
194
|
+
patchSubagentCard(watch.parentTopicId, watch.cardMessageId, {
|
|
195
|
+
status: "completed",
|
|
196
|
+
resultSummary: summary || "(no text response)",
|
|
197
|
+
finishedAt: new Date().toISOString(),
|
|
198
|
+
});
|
|
199
|
+
} catch (err) {
|
|
200
|
+
logger.warn(
|
|
201
|
+
{ err, parentTopicId: watch.parentTopicId, cardMessageId: watch.cardMessageId },
|
|
202
|
+
"subagent: completed-card update failed",
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
await notifyParent(
|
|
206
|
+
watch,
|
|
207
|
+
`[Subagent completed ← ${watch.name}]\n이 메시지는 spawn_subagent로 위임한 작업의 자동 완료 회신입니다. 결과를 확인하고 필요하면 이어서 진행하세요.\n\n${finalText || "(no text response)"}`,
|
|
208
|
+
"done",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Settle a failed child run. NEVER rejects — call sites fire-and-forget. */
|
|
213
|
+
export async function settleSubagentFailure(queryId: string, reason: string): Promise<void> {
|
|
214
|
+
const watch = takeSubagentWatch(queryId);
|
|
215
|
+
if (!watch) return;
|
|
216
|
+
try {
|
|
217
|
+
patchSubagentCard(watch.parentTopicId, watch.cardMessageId, {
|
|
218
|
+
status: "failed",
|
|
219
|
+
errorMessage: reason,
|
|
220
|
+
finishedAt: new Date().toISOString(),
|
|
221
|
+
});
|
|
222
|
+
} catch (err) {
|
|
223
|
+
logger.warn(
|
|
224
|
+
{ err, parentTopicId: watch.parentTopicId, cardMessageId: watch.cardMessageId },
|
|
225
|
+
"subagent: failed-card update failed",
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
await notifyParent(
|
|
229
|
+
watch,
|
|
230
|
+
`[Subagent failed ← ${watch.name}]\nspawn_subagent로 위임한 작업이 실패했습니다: ${reason}`,
|
|
231
|
+
"fail",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Boot-time sweep: watches don't survive a restart, so any card still marked
|
|
237
|
+
* spawned/running belongs to a run whose completion can no longer be tracked.
|
|
238
|
+
*/
|
|
239
|
+
export function sweepStaleSubagentCards(): void {
|
|
240
|
+
for (const msg of listApiMessagesByKind("subagent")) {
|
|
241
|
+
const card = msg.subagentCard;
|
|
242
|
+
if (!card || (card.status !== "spawned" && card.status !== "running")) continue;
|
|
243
|
+
patchSubagentCard(msg.topicId, msg.id, {
|
|
244
|
+
status: "failed",
|
|
245
|
+
errorMessage: "server restarted while the subagent was running",
|
|
246
|
+
finishedAt: new Date().toISOString(),
|
|
247
|
+
});
|
|
248
|
+
logger.info(
|
|
249
|
+
{ topicId: msg.topicId, messageId: msg.id, child: card.subagentTopicId },
|
|
250
|
+
"subagent: swept stale card on boot",
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function spawnSubagent(
|
|
256
|
+
ctx: SpawnSubagentToolContext,
|
|
257
|
+
input: { task?: unknown; name?: unknown; agent?: unknown; model?: unknown },
|
|
258
|
+
) {
|
|
259
|
+
const parent = getTopic(ctx.topicId);
|
|
260
|
+
if (!parent) return errorResult(`Error: topic '${ctx.topicId}' not found.`);
|
|
261
|
+
if (!parent.participants.some((p) => p.userId === ctx.userId)) {
|
|
262
|
+
return errorResult("Error: user is not a member of this topic.");
|
|
263
|
+
}
|
|
264
|
+
if (parent.kind !== "agent" || parent.isSubagent) {
|
|
265
|
+
return errorResult("Error: spawn_subagent is only available in top-level agent rooms.");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const task = typeof input.task === "string" ? input.task.trim() : "";
|
|
269
|
+
if (!task) return errorResult("Error: task is required.");
|
|
270
|
+
if (task.length > MAX_TASK_CHARS) {
|
|
271
|
+
return errorResult(`Error: task is too long (max ${MAX_TASK_CHARS} chars).`);
|
|
272
|
+
}
|
|
273
|
+
const name =
|
|
274
|
+
typeof input.name === "string"
|
|
275
|
+
? input.name
|
|
276
|
+
.replace(/[\n\t]/g, " ")
|
|
277
|
+
.trim()
|
|
278
|
+
.slice(0, MAX_NAME_CHARS)
|
|
279
|
+
: undefined;
|
|
280
|
+
const agentOverride = isAgentKind(input.agent) ? input.agent : undefined;
|
|
281
|
+
const modelOverride =
|
|
282
|
+
typeof input.model === "string" && input.model.trim() ? input.model.trim() : undefined;
|
|
283
|
+
|
|
284
|
+
if (countLiveChildren(ctx.topicId) >= MAX_LIVE_CHILDREN_PER_PARENT) {
|
|
285
|
+
return errorResult(
|
|
286
|
+
`Error: this room already has ${MAX_LIVE_CHILDREN_PER_PARENT} subagents running. Wait for one to finish before spawning another.`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Preflight the overrides before creating anything — a bad agent/model would
|
|
291
|
+
// otherwise leave a dead child room with a card that fails on first turn.
|
|
292
|
+
if (agentOverride) {
|
|
293
|
+
const { checkAgentAuth } = await import("#agents/auth-check");
|
|
294
|
+
const auth = checkAgentAuth(agentOverride);
|
|
295
|
+
if (!auth.ok) {
|
|
296
|
+
return errorResult(`Error: agent '${agentOverride}' is not available: ${auth.error}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (modelOverride) {
|
|
300
|
+
const { getRegistry } = await import("#agents/registry");
|
|
301
|
+
const targetAgent = agentOverride ?? ctx.agent;
|
|
302
|
+
if (!getRegistry(targetAgent).validateModel(modelOverride)) {
|
|
303
|
+
return errorResult(
|
|
304
|
+
`Error: model '${modelOverride}' is not valid for agent '${targetAgent}'.`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const { createDerivedTopic, TopicTitleConflictError } = await import("#topics/derive");
|
|
310
|
+
let child: Awaited<ReturnType<typeof createDerivedTopic>>;
|
|
311
|
+
try {
|
|
312
|
+
child = await createDerivedTopic(ctx.topicId, ctx.userId, false, {
|
|
313
|
+
name,
|
|
314
|
+
subagent: { agent: agentOverride, model: modelOverride },
|
|
315
|
+
});
|
|
316
|
+
} catch (e) {
|
|
317
|
+
if (e instanceof TopicTitleConflictError) {
|
|
318
|
+
return errorResult(`Error: ${e.message}. Try a different name.`);
|
|
319
|
+
}
|
|
320
|
+
throw e;
|
|
321
|
+
}
|
|
322
|
+
if (!child) {
|
|
323
|
+
return errorResult(
|
|
324
|
+
"Error: failed to create the subagent room (restricted or missing source topic).",
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const now = new Date().toISOString();
|
|
329
|
+
const card: SubagentCardDto = {
|
|
330
|
+
subagentTopicId: child.id,
|
|
331
|
+
name: child.title,
|
|
332
|
+
task,
|
|
333
|
+
status: "spawned",
|
|
334
|
+
startedAt: now,
|
|
335
|
+
};
|
|
336
|
+
const cardMsg: MessageDto = {
|
|
337
|
+
id: `subagent-${child.id}`,
|
|
338
|
+
topicId: ctx.topicId,
|
|
339
|
+
authorId: "ai",
|
|
340
|
+
text: `🤖 Subagent "${child.title}" spawned`,
|
|
341
|
+
queryId: ctx.queryId,
|
|
342
|
+
agentType: ctx.agent,
|
|
343
|
+
model: ctx.model,
|
|
344
|
+
kind: "subagent",
|
|
345
|
+
subagentCard: card,
|
|
346
|
+
createdAt: now,
|
|
347
|
+
};
|
|
348
|
+
appendApiMessage(cardMsg, { notify: false });
|
|
349
|
+
WsHub.get().broadcastMessage(ctx.topicId, cardMsg);
|
|
350
|
+
|
|
351
|
+
const watch: SubagentWatch = {
|
|
352
|
+
parentTopicId: ctx.topicId,
|
|
353
|
+
childTopicId: child.id,
|
|
354
|
+
cardMessageId: cardMsg.id,
|
|
355
|
+
name: child.title,
|
|
356
|
+
userId: ctx.userId,
|
|
357
|
+
startedAt: now,
|
|
358
|
+
running: false,
|
|
359
|
+
};
|
|
360
|
+
watchesByChild.set(child.id, watch);
|
|
361
|
+
|
|
362
|
+
let childQueryId: string | null = null;
|
|
363
|
+
let locallyQueued = false;
|
|
364
|
+
const childPrompt = `[Delegated task from ${parent.title}]\n\n${task}`;
|
|
365
|
+
{
|
|
366
|
+
const { triggerTopicAiTurn } = await import("#runtime/turn-runner");
|
|
367
|
+
const { getRoomQuery } = await import("#query/active-rooms");
|
|
368
|
+
childQueryId = triggerTopicAiTurn(child.id, ctx.userId, childPrompt, agentOverride, {
|
|
369
|
+
origin: `subagent-task:${parent.title}`,
|
|
370
|
+
// Required by the defer queue: if a user message preempts the task turn,
|
|
371
|
+
// the re-queue path drops injects that have no requestId.
|
|
372
|
+
requestId: `subagent-task-${child.id}`,
|
|
373
|
+
injectAuthorId: "ai",
|
|
374
|
+
onDispatched: (qid: string) => registerWatchDispatch(watch, qid),
|
|
375
|
+
});
|
|
376
|
+
locallyQueued = Boolean(getRoomQuery(child.id));
|
|
377
|
+
}
|
|
378
|
+
if (!childQueryId && !watch.queryId && !locallyQueued) {
|
|
379
|
+
dropWatch(watch);
|
|
380
|
+
patchSubagentCard(ctx.topicId, cardMsg.id, {
|
|
381
|
+
status: "failed",
|
|
382
|
+
errorMessage: "the subagent turn could not be dispatched",
|
|
383
|
+
finishedAt: new Date().toISOString(),
|
|
384
|
+
});
|
|
385
|
+
return errorResult("Error: subagent room was created but its AI turn could not start.");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
logger.info(
|
|
389
|
+
{ parentTopicId: ctx.topicId, childTopicId: child.id, name: child.title },
|
|
390
|
+
"subagent: spawned",
|
|
391
|
+
);
|
|
392
|
+
return textResult(
|
|
393
|
+
[
|
|
394
|
+
`Subagent "${child.title}" spawned (room id: ${child.id}) and is now working in the background.`,
|
|
395
|
+
"Its final result will be delivered back into this room automatically when it finishes.",
|
|
396
|
+
"Do NOT wait or poll — finish your current turn normally.",
|
|
397
|
+
].join("\n"),
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function ownedDirectSubagents(ctx: SubagentToolContext) {
|
|
402
|
+
const parent = getTopic(ctx.topicId);
|
|
403
|
+
if (!parent) return { ok: false, error: `Error: topic '${ctx.topicId}' not found.` } as const;
|
|
404
|
+
if (!parent.participants.some((participant) => participant.userId === ctx.userId)) {
|
|
405
|
+
return { ok: false, error: "Error: user is not a member of this topic." } as const;
|
|
406
|
+
}
|
|
407
|
+
if (parent.kind !== "agent" || parent.isSubagent) {
|
|
408
|
+
return {
|
|
409
|
+
ok: false,
|
|
410
|
+
error: "Error: subagent management is only available in top-level agent rooms.",
|
|
411
|
+
} as const;
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
ok: true,
|
|
415
|
+
parent,
|
|
416
|
+
children: listTopics().filter(
|
|
417
|
+
(topic) =>
|
|
418
|
+
topic.parentTopicId === ctx.topicId &&
|
|
419
|
+
topic.isSubagent &&
|
|
420
|
+
topic.participants.some(
|
|
421
|
+
(participant) => participant.userId === ctx.userId && participant.role === "owner",
|
|
422
|
+
),
|
|
423
|
+
),
|
|
424
|
+
} as const;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function subagentStatus(
|
|
428
|
+
parentTopicId: string,
|
|
429
|
+
childTopicId: string,
|
|
430
|
+
): SubagentCardDto["status"] | "unknown" {
|
|
431
|
+
const watch = watchesByChild.get(childTopicId);
|
|
432
|
+
if (watch?.running) return "running";
|
|
433
|
+
const card = listApiMessagesByKind("subagent")
|
|
434
|
+
.filter(
|
|
435
|
+
(message) =>
|
|
436
|
+
message.topicId === parentTopicId && message.subagentCard?.subagentTopicId === childTopicId,
|
|
437
|
+
)
|
|
438
|
+
.at(-1)?.subagentCard;
|
|
439
|
+
return card?.status ?? "unknown";
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function createSubagentManagementToolDefinitions(ctx: SubagentToolContext): SharedMcpTool[] {
|
|
443
|
+
return [
|
|
444
|
+
{
|
|
445
|
+
name: "list_subagents",
|
|
446
|
+
description:
|
|
447
|
+
"List the direct subagent rooms created by this user from the current parent room. " +
|
|
448
|
+
"Use this to decide whether a completed subagent should be retained for follow-up work or deleted.",
|
|
449
|
+
schema: {},
|
|
450
|
+
async handler() {
|
|
451
|
+
const result = ownedDirectSubagents(ctx);
|
|
452
|
+
if (!result.ok) return errorResult(result.error);
|
|
453
|
+
const children = result.children.map((child) => ({
|
|
454
|
+
topic_id: child.id,
|
|
455
|
+
name: child.title,
|
|
456
|
+
status: subagentStatus(ctx.topicId, child.id),
|
|
457
|
+
agent: child.agent ?? null,
|
|
458
|
+
model: child.defaultModel ?? null,
|
|
459
|
+
created_at: child.createdAt,
|
|
460
|
+
}));
|
|
461
|
+
return textResult(JSON.stringify({ subagents: children }, null, 2));
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
name: "delete_subagent",
|
|
466
|
+
description:
|
|
467
|
+
"Permanently delete one direct subagent room created by this user from the current parent room. " +
|
|
468
|
+
"This removes its conversation, workspace, runtime state, and topic. Keep it when follow-up work is likely.",
|
|
469
|
+
schema: {
|
|
470
|
+
topic_id: z
|
|
471
|
+
.string()
|
|
472
|
+
.describe("Exact subagent room id returned by list_subagents; names are not accepted."),
|
|
473
|
+
},
|
|
474
|
+
async handler(input) {
|
|
475
|
+
const result = ownedDirectSubagents(ctx);
|
|
476
|
+
if (!result.ok) return errorResult(result.error);
|
|
477
|
+
const topicId = typeof input.topic_id === "string" ? input.topic_id.trim() : "";
|
|
478
|
+
const child = result.children.find((candidate) => candidate.id === topicId);
|
|
479
|
+
if (!child) {
|
|
480
|
+
return errorResult(
|
|
481
|
+
"Error: no owned direct subagent with that topic_id exists under this room.",
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
const { deleteTopicCascade } = await import("#topics/lifecycle");
|
|
486
|
+
await deleteTopicCascade(child, ctx.userId);
|
|
487
|
+
return textResult(`Subagent deleted: ${child.title} (${child.id})`);
|
|
488
|
+
} catch (err) {
|
|
489
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
490
|
+
return errorResult(`Error: failed to delete subagent '${child.title}': ${message}`);
|
|
491
|
+
}
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function createSpawnSubagentToolDefinition(ctx: SpawnSubagentToolContext): SharedMcpTool {
|
|
498
|
+
return {
|
|
499
|
+
name: "spawn_subagent",
|
|
500
|
+
description:
|
|
501
|
+
"Delegate a self-contained task to a subagent that works in its own new agent room. " +
|
|
502
|
+
"Returns immediately; the subagent runs in the background and its final result is injected back into this room when it finishes. " +
|
|
503
|
+
"The subagent starts with ONLY the task text — include all necessary context, file paths, and acceptance criteria in it. " +
|
|
504
|
+
"Use for parallelizable or long-running side work. Do not use provider built-in Task/Agent subagents.",
|
|
505
|
+
schema: {
|
|
506
|
+
task: z
|
|
507
|
+
.string()
|
|
508
|
+
.describe(
|
|
509
|
+
"Self-contained task brief for the subagent. It sees nothing else — include all context.",
|
|
510
|
+
),
|
|
511
|
+
name: z
|
|
512
|
+
.string()
|
|
513
|
+
.optional()
|
|
514
|
+
.describe("Short name for the subagent room. Auto-generated when omitted."),
|
|
515
|
+
agent: z
|
|
516
|
+
.enum(["claude", "codex", "maestro"])
|
|
517
|
+
.optional()
|
|
518
|
+
.describe("Agent backend override. Defaults to this room's agent."),
|
|
519
|
+
model: z.string().optional().describe("Model override. Defaults to the agent's default."),
|
|
520
|
+
},
|
|
521
|
+
async handler(input) {
|
|
522
|
+
return spawnSubagent(ctx, input);
|
|
523
|
+
},
|
|
524
|
+
};
|
|
525
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { errorResult, type SharedMcpTool, textResult } from "#agents/mcp-tools/common";
|
|
3
|
+
|
|
4
|
+
export const VISUALS_MCP_KEY = "visuals";
|
|
5
|
+
|
|
6
|
+
export const showHtmlTool = {
|
|
7
|
+
name: "show_html",
|
|
8
|
+
description:
|
|
9
|
+
"Display charts, tables, or interactive HTML to the user in a sandboxed WebView side panel. Pass a complete, self-contained HTML string with inline CSS/JS. Local DOM interactions are supported. Links (<a href> to http/https URLs) are safe to include: a click opens the URL in the user's system browser. Network requests, external scripts, form posts, popups, and parent-window access are blocked.",
|
|
10
|
+
schema: {
|
|
11
|
+
html: z.string().describe("Complete, self-contained HTML document or fragment to render."),
|
|
12
|
+
title: z.string().optional().describe("Optional title shown above the rendered card."),
|
|
13
|
+
},
|
|
14
|
+
async handler() {
|
|
15
|
+
return textResult("HTML card displayed to the user.");
|
|
16
|
+
},
|
|
17
|
+
} satisfies SharedMcpTool;
|
|
18
|
+
|
|
19
|
+
export const showMermaidTool = {
|
|
20
|
+
name: "show_mermaid",
|
|
21
|
+
description:
|
|
22
|
+
"Render a Mermaid diagram in the user's visual side panel. Pass Mermaid DSL only, not markdown fences. Use this for flowcharts, sequence diagrams, class diagrams, state diagrams, ER diagrams, timelines, and architecture diagrams.",
|
|
23
|
+
schema: {
|
|
24
|
+
code: z.string().describe("Mermaid diagram source without markdown code fences."),
|
|
25
|
+
title: z.string().optional().describe("Optional title shown above the rendered card."),
|
|
26
|
+
theme: z
|
|
27
|
+
.enum(["default", "neutral", "dark", "forest"])
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Optional Mermaid theme. Defaults to neutral."),
|
|
30
|
+
},
|
|
31
|
+
async handler() {
|
|
32
|
+
return textResult("Mermaid diagram displayed to the user.");
|
|
33
|
+
},
|
|
34
|
+
} satisfies SharedMcpTool;
|
|
35
|
+
|
|
36
|
+
export const showImageTool = {
|
|
37
|
+
name: "show_image",
|
|
38
|
+
description:
|
|
39
|
+
"Display an image in the user's visual side panel. Provide either file_path for an image in the topic workspace, or file_id for an uploaded file already attached in this topic.",
|
|
40
|
+
schema: {
|
|
41
|
+
file_path: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Absolute or workspace-relative path to an image file."),
|
|
45
|
+
file_id: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.describe("Full Otium uploaded file UUID for an image attached in this topic."),
|
|
49
|
+
title: z.string().optional().describe("Optional title shown above the rendered card."),
|
|
50
|
+
alt: z.string().optional().describe("Optional alt text for the image."),
|
|
51
|
+
},
|
|
52
|
+
async handler(input) {
|
|
53
|
+
if (!input?.file_path && !input?.file_id) {
|
|
54
|
+
return errorResult("file_path or file_id is required.");
|
|
55
|
+
}
|
|
56
|
+
return textResult("Image displayed to the user.");
|
|
57
|
+
},
|
|
58
|
+
} satisfies SharedMcpTool;
|
|
59
|
+
|
|
60
|
+
export const showVideoTool = {
|
|
61
|
+
name: "show_video",
|
|
62
|
+
description:
|
|
63
|
+
"Display a playable video in the user's visual side panel. Provide either file_path for a video in the topic workspace, or file_id for an uploaded file already attached in this topic.",
|
|
64
|
+
schema: {
|
|
65
|
+
file_path: z
|
|
66
|
+
.string()
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Absolute or workspace-relative path to a video file."),
|
|
69
|
+
file_id: z
|
|
70
|
+
.string()
|
|
71
|
+
.optional()
|
|
72
|
+
.describe("Full Otium uploaded file UUID for a video attached in this topic."),
|
|
73
|
+
title: z.string().optional().describe("Optional title shown above the rendered card."),
|
|
74
|
+
},
|
|
75
|
+
async handler(input) {
|
|
76
|
+
if (!input?.file_path && !input?.file_id) {
|
|
77
|
+
return errorResult("file_path or file_id is required.");
|
|
78
|
+
}
|
|
79
|
+
return textResult("Video displayed to the user.");
|
|
80
|
+
},
|
|
81
|
+
} satisfies SharedMcpTool;
|
|
82
|
+
|
|
83
|
+
export const visualToolDefinitions = [
|
|
84
|
+
showHtmlTool,
|
|
85
|
+
showMermaidTool,
|
|
86
|
+
showImageTool,
|
|
87
|
+
showVideoTool,
|
|
88
|
+
] as const;
|