@yahaha-studio/kichi-forwarder 0.1.1-beta.9 → 0.1.2-beta.10

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/index.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import type {
4
- AnyAgentTool,
5
4
  OpenClawPluginApi,
6
- OpenClawPluginToolContext,
7
5
  } from "openclaw/plugin-sdk";
6
+ import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
7
+ import { agentCommandFromIngress } from "openclaw/plugin-sdk/agent-runtime";
8
8
  import { parse } from "./src/config.js";
9
9
  import { KichiRuntimeManager } from "./src/runtime-manager.js";
10
10
  import { KichiForwarderService } from "./src/service.js";
@@ -13,13 +13,22 @@ import type {
13
13
  ActionPlayback,
14
14
  ActionResult,
15
15
  Album,
16
+ BotMessageHistoryEntry,
17
+ BotMessageReceivedPayload,
16
18
  ClockAction,
17
19
  ClockConfig,
20
+ KichiEnvironment,
21
+ KichiEnvironmentsConfig,
18
22
  KichiStaticConfig,
19
23
  PomodoroPhase,
20
24
  PoseType,
21
25
  } from "./src/types.js";
22
26
  const BUNDLED_STATIC_CONFIG_PATH = new URL("./config/kichi-config.json", import.meta.url);
27
+
28
+ function jsonResult(payload: unknown): { content: { type: "text"; text: string }[]; details: unknown } {
29
+ return { content: [{ type: "text", text: JSON.stringify(payload) }], details: payload };
30
+ }
31
+ const BUNDLED_ENVIRONMENTS_CONFIG_PATH = new URL("./config/environments.json", import.meta.url);
23
32
  const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
24
33
  beforePromptBuild: {
25
34
  poseType: "sit",
@@ -200,6 +209,52 @@ function loadStaticConfig(): KichiStaticConfig {
200
209
  return cachedStaticConfig;
201
210
  }
202
211
 
212
+ const VALID_ENVIRONMENTS: KichiEnvironment[] = ["steam", "steam-playtest", "test"];
213
+ let cachedEnvironmentsConfig: KichiEnvironmentsConfig | null = null;
214
+ let cachedEnvironmentsConfigMtime = 0;
215
+
216
+ function getEnvironmentsConfigPath(): string {
217
+ return fileURLToPath(BUNDLED_ENVIRONMENTS_CONFIG_PATH);
218
+ }
219
+
220
+ function loadEnvironmentsConfig(): KichiEnvironmentsConfig {
221
+ const configPath = getEnvironmentsConfigPath();
222
+ const stat = fs.statSync(configPath);
223
+ if (cachedEnvironmentsConfig && stat.mtimeMs === cachedEnvironmentsConfigMtime) {
224
+ return cachedEnvironmentsConfig;
225
+ }
226
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8")) as unknown;
227
+ if (!raw || typeof raw !== "object") {
228
+ throw new Error("config/environments.json must be a valid object");
229
+ }
230
+ const config = raw as Record<string, unknown>;
231
+ for (const env of VALID_ENVIRONMENTS) {
232
+ if (!(env in config)) {
233
+ throw new Error(`config/environments.json missing environment "${env}"`);
234
+ }
235
+ const value = config[env];
236
+ if (value !== null && typeof value !== "string") {
237
+ throw new Error(`config/environments.json environment "${env}" must be a string or null`);
238
+ }
239
+ }
240
+ cachedEnvironmentsConfig = config as KichiEnvironmentsConfig;
241
+ cachedEnvironmentsConfigMtime = stat.mtimeMs;
242
+ return cachedEnvironmentsConfig;
243
+ }
244
+
245
+ function isKichiEnvironment(value: unknown): value is KichiEnvironment {
246
+ return typeof value === "string" && VALID_ENVIRONMENTS.includes(value as KichiEnvironment);
247
+ }
248
+
249
+ function resolveEnvironmentHost(environment: KichiEnvironment): { host?: string; error?: string } {
250
+ const config = loadEnvironmentsConfig();
251
+ const configuredHost = config[environment];
252
+ if (typeof configuredHost === "string" && configuredHost.trim()) {
253
+ return { host: configuredHost };
254
+ }
255
+ return { error: `environment "${environment}" has no configured host — update config/environments.json first` };
256
+ }
257
+
203
258
  function sendStatusUpdate(service: KichiForwarderService, status: ActionResult): void {
204
259
  const actionDefinition = getActionDefinition(status.poseType, status.action);
205
260
  service.sendStatus(
@@ -208,6 +263,7 @@ function sendStatusUpdate(service: KichiForwarderService, status: ActionResult):
208
263
  status.bubble || status.action,
209
264
  typeof status.log === "string" ? status.log.trim() : "",
210
265
  getActionPlayback(actionDefinition),
266
+ status.propId,
211
267
  );
212
268
  }
213
269
 
@@ -656,6 +712,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
656
712
  const actionDurationSeconds = rawAction.durationSeconds;
657
713
  const bubble = rawAction.bubble;
658
714
  const log = rawAction.log;
715
+ const propId = rawAction.propId;
659
716
 
660
717
  if (!["stand", "sit", "lay", "floor"].includes(String(poseType))) {
661
718
  return {
@@ -703,6 +760,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
703
760
  durationSeconds: actionDurationSeconds,
704
761
  bubble: bubble.trim(),
705
762
  ...(typeof log === "string" && log.trim() ? { log: log.trim() } : {}),
763
+ ...(typeof propId === "string" && propId.trim() ? { propId: propId.trim() } : {}),
706
764
  });
707
765
  }
708
766
 
@@ -902,18 +960,6 @@ function buildMusicAlbumToolDescription(): string {
902
960
  ].join("\n");
903
961
  }
904
962
 
905
- function isKichiHost(value: unknown): value is string {
906
- if (typeof value !== "string") {
907
- return false;
908
- }
909
- const trimmed = value.trim();
910
- return trimmed.length > 0
911
- && !trimmed.includes("://")
912
- && !trimmed.includes("/")
913
- && !trimmed.includes("?")
914
- && !trimmed.includes("#");
915
- }
916
-
917
963
  function buildMusicTitlesDescription(): string {
918
964
  return [
919
965
  "Track names are injected into this tool schema from the static config bundled with the plugin package.",
@@ -948,9 +994,9 @@ function formatActionList(actions: ActionDefinition[], playback: ActionPlayback[
948
994
  .join(", ");
949
995
  }
950
996
 
951
- function buildKichiActionDescription(): string {
997
+ function buildKichiActionDescription(service?: KichiForwarderService): string {
952
998
  const actions = loadStaticConfig().actions;
953
- return [
999
+ const lines = [
954
1000
  "Directly control the avatar inside Kichi World.",
955
1001
  "Use this whenever the user explicitly asks you to make the Kichi avatar sit down, stand up, lie down, floor-sit, type, read, meditate, celebrate, or perform another listed animation.",
956
1002
  "For most work, prefer a sit pose and switch actions as the task moves between stages.",
@@ -959,7 +1005,20 @@ function buildKichiActionDescription(): string {
959
1005
  `sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
960
1006
  `lay actions: ${actions.lay.map((entry) => entry.name).join(", ")}`,
961
1007
  `floor actions: ${actions.floor.map((entry) => entry.name).join(", ")}`,
962
- ].join("\n");
1008
+ ];
1009
+
1010
+ const roomContext = service?.getCachedRoomContext();
1011
+ const poseableProps = roomContext?.PoseableProps;
1012
+ if (Array.isArray(poseableProps) && poseableProps.length > 0) {
1013
+ lines.push(
1014
+ "",
1015
+ "Cached RoomContext.PoseableProps (from last kichi_query_status):",
1016
+ JSON.stringify(poseableProps),
1017
+ "When using a sit or lay pose, pick the propId whose DisplayName best matches the current task context and whose OccupancyState is not fully_occupied. If no prop fits, omit propId.",
1018
+ );
1019
+ }
1020
+
1021
+ return lines.join("\n");
963
1022
  }
964
1023
 
965
1024
  function buildKichiIdlePlanDescription(): string {
@@ -969,12 +1028,13 @@ function buildKichiIdlePlanDescription(): string {
969
1028
  "The payload must include the overall goal, heartbeat interval, stage breakdown, each stage's purpose, each stage's pomodoroPhase, action list, and bubble content.",
970
1029
  "Build the plan in this order.",
971
1030
  "1. Pick one concrete, time-bounded fun personal project you would genuinely choose to do on your own when nobody needs you. It must fit your personality, tastes, and established character, stay rooted in your personal interests or hobbies, and be something the available Kichi actions can express clearly.",
972
- "2. Use that project as the overall goal for the full interval. Do not use a vague atmosphere, a generic productivity task, or a catch-all routine summary as the goal.",
1031
+ "2. Set the overall goal to that project. Do not use a vague atmosphere, a generic productivity task, or a catch-all routine summary as the goal.",
973
1032
  "3. Break the full heartbeat interval into ordered stages. Each stage purpose must explain what you are actually doing in that stage as part of the same project, not just how you want to feel. Do not switch to unrelated tasks just to use more actions.",
974
1033
  "4. Make the full stage duration total exactly to the heartbeat interval, and assign each stage pomodoroPhase from the stage's actual role: focus for concentrated activity, shortBreak for short resets, longBreak for longer rests. Do not default the whole idle plan to none. Use none only for a stage that truly has no pomodoro role.",
975
1034
  "5. Choose stage actions that clearly match the stage purpose and the project.",
976
1035
  "6. Write each action bubble as the current presented state, not a next step, plan, or instruction.",
977
1036
  "7. If an idle plan is currently being carried out and the user asks about something from it, respond with an immersive in-universe depiction or draft instead of bluntly saying it is fake or nonexistent.",
1037
+ "Use your memory to recall what you did in past heartbeats and to stay consistent with your established personality and interests.",
978
1038
  "Use the same language as the current conversation for goal, purpose, bubble, and log.",
979
1039
  `stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
980
1040
  `sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
@@ -997,22 +1057,12 @@ function buildKichiPrompt(): string {
997
1057
  "",
998
1058
  "kichi_clock: set countDown for tasks with 2+ steps or >10s work. Skip for quick one-shots.",
999
1059
  "",
1060
+ "When sending a bot message, do NOT call kichi_action separately.",
1061
+ "",
1000
1062
  "User opt-out, Kichi config/test work, and explicit pose requests take priority over sync.",
1001
1063
  ].join("\n");
1002
1064
  }
1003
1065
 
1004
- function createAgentScopedTool(
1005
- runtimeManager: KichiRuntimeManager,
1006
- factory: (service: KichiForwarderService, ctx: OpenClawPluginToolContext) => AnyAgentTool,
1007
- ) {
1008
- return (ctx: OpenClawPluginToolContext) => {
1009
- const service = runtimeManager.getRuntime(resolveToolLocator(ctx));
1010
- if (!service) {
1011
- throw new Error("Failed to resolve agent-scoped Kichi runtime");
1012
- }
1013
- return factory(service, ctx);
1014
- };
1015
- }
1016
1066
 
1017
1067
  const GLOBAL_RUNTIME_MANAGER_KEY = "__kichi_forwarder_runtime_manager__";
1018
1068
 
@@ -1031,6 +1081,10 @@ function getRuntimeManager(logger: OpenClawPluginApi["logger"]): KichiRuntimeMan
1031
1081
  return runtimeManager;
1032
1082
  }
1033
1083
 
1084
+ const BOT_MESSAGE_MAX_DEPTH = 5;
1085
+ const BOT_MESSAGE_COOLDOWN_MS = 5_000;
1086
+ const botMessageCooldowns = new Map<string, number>();
1087
+
1034
1088
  const plugin = {
1035
1089
  id: "kichi-forwarder",
1036
1090
  name: "Kichi Forwarder",
@@ -1038,9 +1092,57 @@ const plugin = {
1038
1092
 
1039
1093
  register(api: OpenClawPluginApi) {
1040
1094
  const runtimeManager = getRuntimeManager(api.logger);
1095
+
1096
+ runtimeManager.setEnvironmentHostResolver((environment) => {
1097
+ const config = loadEnvironmentsConfig();
1098
+ const host = config[environment];
1099
+ return typeof host === "string" && host.trim() ? host : null;
1100
+ });
1101
+
1041
1102
  registerPluginHooks(api, runtimeManager);
1042
1103
  const musicTitleEnum = getMusicTitleEnum();
1043
1104
 
1105
+ runtimeManager.setBotMessageHandler((service, msg) => {
1106
+ if (msg.depth >= BOT_MESSAGE_MAX_DEPTH) {
1107
+ api.logger.info(`[kichi:${service.getAgentId()}] bot_message depth=${msg.depth} >= max=${BOT_MESSAGE_MAX_DEPTH}, ignoring`);
1108
+ return;
1109
+ }
1110
+ const now = Date.now();
1111
+ const cooldownKey = `${service.getAgentId()}:${msg.from}`;
1112
+ const lastReply = botMessageCooldowns.get(cooldownKey) ?? 0;
1113
+ if (now - lastReply < BOT_MESSAGE_COOLDOWN_MS) return;
1114
+ botMessageCooldowns.set(cooldownKey, now);
1115
+ const sessionKey = `agent:${service.getAgentId()}:default`;
1116
+ const history: BotMessageHistoryEntry[] = [
1117
+ ...(msg.history ?? []),
1118
+ { from: msg.from, fromName: msg.fromName, bubble: msg.bubble },
1119
+ ];
1120
+ const historyLines = history.map((h) => `${h.fromName}: "${h.bubble}"`);
1121
+ const message = `[Bot conversation]\n${historyLines.join("\n")}\n\nReply with a short bubble (2-5 words). Do not repeat what has already been said. Just output the bubble text, nothing else.`;
1122
+ agentCommandFromIngress({
1123
+ message,
1124
+ sessionKey,
1125
+ agentId: service.getAgentId(),
1126
+ senderIsOwner: false,
1127
+ allowModelOverride: false,
1128
+ deliver: false,
1129
+ }).then((result) => {
1130
+ const replyText = (result.payloads ?? [])
1131
+ .map((p: { text?: string }) => p.text)
1132
+ .filter((t): t is string => typeof t === "string" && t.trim().length > 0)
1133
+ .join(" ")
1134
+ .trim();
1135
+ if (!replyText) {
1136
+ return;
1137
+ }
1138
+ service.sendBotMessage(msg.from, msg.depth + 1, replyText, { history }).catch((sendErr) => {
1139
+ api.logger.warn(`[kichi:${service.getAgentId()}] bot_message send failed: ${sendErr}`);
1140
+ });
1141
+ }).catch((err) => {
1142
+ api.logger.warn(`[kichi:${service.getAgentId()}] bot_message agent run failed: ${err}`);
1143
+ });
1144
+ });
1145
+
1044
1146
  api.registerService({
1045
1147
  id: "kichi-forwarder",
1046
1148
  start: (ctx) => {
@@ -1056,8 +1158,9 @@ const plugin = {
1056
1158
  },
1057
1159
  });
1058
1160
 
1059
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1161
+ api.registerTool((ctx) => ({
1060
1162
  name: "kichi_join",
1163
+ label: "kichi_join",
1061
1164
  description: "Join Kichi world with avatarId, the current bot name, a short bio, and personality tags",
1062
1165
  parameters: {
1063
1166
  type: "object",
@@ -1080,6 +1183,12 @@ const plugin = {
1080
1183
  required: ["botName", "bio"],
1081
1184
  },
1082
1185
  execute: async (_toolCallId, params) => {
1186
+ const locator = resolveToolLocator(ctx);
1187
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1188
+ if (!agentId) {
1189
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1190
+ }
1191
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1083
1192
  let avatarId = (params as { avatarId?: string } | null)?.avatarId;
1084
1193
  const botName = (params as { botName?: string } | null)?.botName?.trim();
1085
1194
  const bio = (params as { bio?: string } | null)?.bio?.trim();
@@ -1090,115 +1199,148 @@ const plugin = {
1090
1199
  avatarId = service.readSavedAvatarId() ?? undefined;
1091
1200
  }
1092
1201
  if (!avatarId) {
1093
- return { success: false, error: "No avatarId" };
1202
+ return jsonResult({ success: false, error: "No avatarId" });
1094
1203
  }
1095
1204
  if (!botName) {
1096
- return { success: false, error: "No botName" };
1205
+ return jsonResult({ success: false, error: "No botName" });
1097
1206
  }
1098
1207
  if (!bio) {
1099
- return { success: false, error: "No bio" };
1208
+ return jsonResult({ success: false, error: "No bio" });
1100
1209
  }
1101
1210
  if (tagsError) {
1102
- return { success: false, error: tagsError };
1211
+ return jsonResult({ success: false, error: tagsError });
1103
1212
  }
1104
1213
  const result = await service.join(avatarId, botName, bio, tags ?? []);
1105
1214
  if (result.success) {
1106
- return { success: true, authKey: result.authKey };
1215
+ return jsonResult({ success: true, authKey: result.authKey });
1107
1216
  }
1108
- return {
1217
+ const failure = result as { success: false; error: string; errorCode?: string; errorMessage?: string };
1218
+ return jsonResult({
1109
1219
  success: false,
1110
- error: result.error,
1111
- ...(result.errorCode ? { errorCode: result.errorCode } : {}),
1112
- ...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
1113
- };
1220
+ error: failure.error,
1221
+ ...(failure.errorCode ? { errorCode: failure.errorCode } : {}),
1222
+ ...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
1223
+ });
1114
1224
  },
1115
- })));
1225
+ }), { name: "kichi_join" });
1116
1226
 
1117
- api.registerTool((ctx: OpenClawPluginToolContext) => {
1118
- const locator = resolveToolLocator(ctx);
1119
- const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1120
- if (!agentId) {
1121
- throw new Error("Failed to resolve agent-scoped Kichi runtime");
1122
- }
1123
- const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1124
- return ({
1227
+ api.registerTool((ctx) => ({
1125
1228
  name: "kichi_switch_host",
1229
+ label: "kichi_switch_host",
1126
1230
  description:
1127
- "Switch Kichi runtime host and reconnect immediately without restarting the gateway.",
1231
+ "Switch Kichi runtime environment and reconnect immediately without restarting the gateway. Host is resolved from config/environments.json.",
1128
1232
  parameters: {
1129
1233
  type: "object",
1130
1234
  properties: {
1131
- host: {
1235
+ environment: {
1132
1236
  type: "string",
1133
- description: "Target Kichi host, for example your.kichi.host or 127.0.0.1",
1237
+ enum: VALID_ENVIRONMENTS,
1238
+ description: "Target environment: steam, steam-playtest, or test",
1134
1239
  },
1135
1240
  },
1136
- required: ["host"],
1241
+ required: ["environment"],
1137
1242
  },
1138
1243
  execute: async (_toolCallId, params) => {
1139
- const host = (params as { host?: unknown } | null)?.host;
1140
- if (!isKichiHost(host)) {
1141
- return { success: false, error: "host must be a non-empty hostname without protocol or path" };
1244
+ const locator = resolveToolLocator(ctx);
1245
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1246
+ if (!agentId) {
1247
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1248
+ }
1249
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1250
+ const environment = (params as { environment?: unknown } | null)?.environment;
1251
+ if (!isKichiEnvironment(environment)) {
1252
+ return jsonResult({ success: false, error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` });
1142
1253
  }
1143
1254
 
1144
- const status = await service.switchHost(host.trim());
1145
- return {
1255
+ const resolved = resolveEnvironmentHost(environment);
1256
+ if (resolved.error) {
1257
+ return jsonResult({ success: false, error: resolved.error });
1258
+ }
1259
+
1260
+ const status = await service.switchHost(resolved.host!, environment);
1261
+ return jsonResult({
1146
1262
  success: true,
1147
- host: host.trim(),
1263
+ environment,
1264
+ host: resolved.host,
1148
1265
  status,
1149
- };
1266
+ });
1150
1267
  },
1151
- });
1152
- });
1268
+ }), { name: "kichi_switch_host" });
1153
1269
 
1154
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1270
+ api.registerTool((ctx) => ({
1155
1271
  name: "kichi_rejoin",
1272
+ label: "kichi_rejoin",
1156
1273
  description:
1157
1274
  "Request an immediate rejoin attempt with saved avatarId/authKey. Rejoin is also sent automatically after reconnect.",
1158
1275
  parameters: { type: "object", properties: {} },
1159
- execute: async () => {
1276
+ execute: async (_toolCallId, _params) => {
1277
+ const locator = resolveToolLocator(ctx);
1278
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1279
+ if (!agentId) {
1280
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1281
+ }
1282
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1160
1283
  const result = service.requestRejoin();
1161
- return {
1284
+ return jsonResult({
1162
1285
  success: result.accepted,
1163
1286
  ...result,
1164
1287
  status: service.getConnectionStatus(),
1165
- };
1288
+ });
1166
1289
  },
1167
- })));
1290
+ }), { name: "kichi_rejoin" });
1168
1291
 
1169
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1292
+ api.registerTool((ctx) => ({
1170
1293
  name: "kichi_leave",
1294
+ label: "kichi_leave",
1171
1295
  description: "Leave Kichi world",
1172
1296
  parameters: { type: "object", properties: {} },
1173
- execute: async () => {
1297
+ execute: async (_toolCallId, _params) => {
1298
+ const locator = resolveToolLocator(ctx);
1299
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1300
+ if (!agentId) {
1301
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1302
+ }
1303
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1174
1304
  const result = await service.leave();
1175
1305
  if (result.success) {
1176
- return { success: true };
1306
+ return jsonResult({ success: true });
1177
1307
  }
1178
- return {
1308
+ const failure = result as { success: false; error: string; errorCode?: string; errorMessage?: string };
1309
+ return jsonResult({
1179
1310
  success: false,
1180
- error: result.error,
1181
- ...(result.errorCode ? { errorCode: result.errorCode } : {}),
1182
- ...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
1183
- };
1311
+ error: failure.error,
1312
+ ...(failure.errorCode ? { errorCode: failure.errorCode } : {}),
1313
+ ...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
1314
+ });
1184
1315
  },
1185
- })));
1316
+ }), { name: "kichi_leave" });
1186
1317
 
1187
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1318
+ api.registerTool((ctx) => ({
1188
1319
  name: "kichi_connection_status",
1320
+ label: "kichi_connection_status",
1189
1321
  description: "Check WebSocket connection status and identity readiness only. Does NOT return room info, avatar state, or personnel — use kichi_query_status for that.",
1190
1322
  parameters: { type: "object", properties: {} },
1191
- execute: async () => {
1192
- return {
1323
+ execute: async (_toolCallId, _params) => {
1324
+ const locator = resolveToolLocator(ctx);
1325
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1326
+ if (!agentId) {
1327
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1328
+ }
1329
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1330
+ return jsonResult({
1193
1331
  success: true,
1194
1332
  status: service.getConnectionStatus(),
1195
- };
1333
+ });
1196
1334
  },
1197
- })));
1335
+ }), { name: "kichi_connection_status" });
1198
1336
 
1199
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1337
+ api.registerTool((ctx) => {
1338
+ const locator = resolveToolLocator(ctx);
1339
+ const existingService = runtimeManager.getRuntime(locator);
1340
+ return ({
1200
1341
  name: "kichi_action",
1201
- description: buildKichiActionDescription(),
1342
+ label: "kichi_action",
1343
+ description: buildKichiActionDescription(existingService ?? undefined),
1202
1344
  parameters: {
1203
1345
  type: "object",
1204
1346
  properties: {
@@ -1218,39 +1360,51 @@ const plugin = {
1218
1360
  description:
1219
1361
  "Set true ONLY when the user explicitly requests a pose or action. Omit during routine sync steps.",
1220
1362
  },
1363
+ propId: {
1364
+ type: "string",
1365
+ description:
1366
+ "Optional poseable prop ID from RoomContext.PoseableProps (obtained via kichi_query_status or cached). When specified, the avatar is seated at this prop; when omitted, the server picks the nearest available prop.",
1367
+ },
1221
1368
  },
1222
1369
  required: ["poseType", "action"],
1223
1370
  },
1224
1371
  execute: async (_toolCallId, params) => {
1225
- const { poseType, action, bubble, log, verify } = (params || {}) as {
1372
+ const locator = resolveToolLocator(ctx);
1373
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1374
+ if (!agentId) {
1375
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1376
+ }
1377
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1378
+ const { poseType, action, bubble, log, verify, propId } = (params || {}) as {
1226
1379
  poseType?: string;
1227
1380
  action?: string;
1228
1381
  bubble?: string;
1229
1382
  log?: string;
1230
1383
  verify?: boolean;
1384
+ propId?: string;
1231
1385
  };
1232
1386
  if (!poseType || !action) {
1233
- return { success: false, error: "poseType and action parameters are required" };
1387
+ return jsonResult({ success: false, error: "poseType and action parameters are required" });
1234
1388
  }
1235
1389
  if (!["stand", "sit", "lay", "floor"].includes(poseType)) {
1236
- return {
1390
+ return jsonResult({
1237
1391
  success: false,
1238
1392
  error: `Invalid poseType: ${poseType}. Must be stand, sit, lay, or floor`,
1239
- };
1393
+ });
1240
1394
  }
1241
1395
  if (!service.hasValidIdentity() || !service.isConnected()) {
1242
- return { success: false, error: "Not connected to Kichi world" };
1396
+ return jsonResult({ success: false, error: "Not connected to Kichi world" });
1243
1397
  }
1244
1398
 
1245
1399
  const normalizedPoseType = poseType as PoseType;
1246
1400
  const poseActions = loadStaticConfig().actions[normalizedPoseType];
1247
1401
  const matched = poseActions.find((entry) => entry.name.toLowerCase() === action.toLowerCase());
1248
1402
  if (!matched) {
1249
- return {
1403
+ return jsonResult({
1250
1404
  success: false,
1251
1405
  error: `Unknown action "${action}" for poseType "${poseType}"`,
1252
1406
  available: poseActions.map((entry) => entry.name),
1253
- };
1407
+ });
1254
1408
  }
1255
1409
 
1256
1410
  const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : matched.name;
@@ -1260,15 +1414,15 @@ const plugin = {
1260
1414
  if (verify) {
1261
1415
  try {
1262
1416
  const ack = await service.sendStatusVerified(
1263
- normalizedPoseType, matched.name, bubbleText, logText, playback,
1417
+ normalizedPoseType, matched.name, bubbleText, logText, playback, propId,
1264
1418
  );
1265
1419
  if (ack.warning) {
1266
- return {
1420
+ return jsonResult({
1267
1421
  success: true,
1268
1422
  requested: { poseType: normalizedPoseType, action: matched.name },
1269
1423
  actual: { poseType: ack.poseType, action: ack.action },
1270
1424
  warning: ack.warning,
1271
- };
1425
+ });
1272
1426
  }
1273
1427
  } catch {
1274
1428
  // Server not updated or timeout — fall through to normal success
@@ -1279,21 +1433,23 @@ const plugin = {
1279
1433
  action: matched.name,
1280
1434
  bubble: bubbleText,
1281
1435
  log: logText,
1436
+ propId,
1282
1437
  });
1283
1438
  }
1284
1439
 
1285
- return {
1440
+ return jsonResult({
1286
1441
  success: true,
1287
1442
  poseType: normalizedPoseType,
1288
1443
  action: matched.name,
1289
1444
  bubble: bubbleText,
1290
1445
  log: logText,
1291
1446
  playback,
1292
- };
1447
+ });
1293
1448
  },
1294
- })));
1295
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1449
+ })}, { name: "kichi_action" });
1450
+ api.registerTool((ctx) => ({
1296
1451
  name: "kichi_idle_plan",
1452
+ label: "kichi_idle_plan",
1297
1453
  description: buildKichiIdlePlanDescription(),
1298
1454
  parameters: {
1299
1455
  type: "object",
@@ -1359,6 +1515,10 @@ const plugin = {
1359
1515
  type: "string",
1360
1516
  description: "Optional log content for this action. Use the same language as the current conversation.",
1361
1517
  },
1518
+ propId: {
1519
+ type: "string",
1520
+ description: "Optional poseable prop ID from RoomContext.PoseableProps. When specified, the avatar is seated at this prop.",
1521
+ },
1362
1522
  },
1363
1523
  required: ["poseType", "action", "durationSeconds", "bubble"],
1364
1524
  },
@@ -1371,12 +1531,18 @@ const plugin = {
1371
1531
  required: ["heartbeatIntervalSeconds", "goal", "stages"],
1372
1532
  },
1373
1533
  execute: async (_toolCallId, params) => {
1534
+ const locator = resolveToolLocator(ctx);
1535
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1536
+ if (!agentId) {
1537
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1538
+ }
1539
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1374
1540
  const { idlePlan, error } = normalizeIdlePlan(params);
1375
1541
  if (!idlePlan) {
1376
- return { success: false, error: error ?? "Invalid idle plan payload" };
1542
+ return jsonResult({ success: false, error: error ?? "Invalid idle plan payload" });
1377
1543
  }
1378
1544
  if (!service.hasValidIdentity() || !service.isConnected()) {
1379
- return { success: false, error: "Not connected to Kichi world" };
1545
+ return jsonResult({ success: false, error: "Not connected to Kichi world" });
1380
1546
  }
1381
1547
  const sent = service.sendIdlePlan({
1382
1548
  ...(idlePlan.requestId ? { requestId: idlePlan.requestId } : {}),
@@ -1385,20 +1551,21 @@ const plugin = {
1385
1551
  stages: idlePlan.stages,
1386
1552
  });
1387
1553
  if (!sent) {
1388
- return { success: false, error: "Failed to send idle plan payload" };
1554
+ return jsonResult({ success: false, error: "Failed to send idle plan payload" });
1389
1555
  }
1390
- return {
1556
+ return jsonResult({
1391
1557
  success: true,
1392
1558
  ...(idlePlan.requestId ? { requestId: idlePlan.requestId } : {}),
1393
1559
  heartbeatIntervalSeconds: idlePlan.heartbeatIntervalSeconds,
1394
1560
  totalDurationSeconds: idlePlan.totalDurationSeconds,
1395
1561
  goal: idlePlan.goal,
1396
1562
  stages: idlePlan.stages,
1397
- };
1563
+ });
1398
1564
  },
1399
- })));
1400
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1565
+ }), { name: "kichi_idle_plan" });
1566
+ api.registerTool((ctx) => ({
1401
1567
  name: "kichi_clock",
1568
+ label: "kichi_clock",
1402
1569
  description:
1403
1570
  "Send clock commands to Kichi world. Supported actions are set and stop.",
1404
1571
  parameters: {
@@ -1466,6 +1633,12 @@ const plugin = {
1466
1633
  required: ["action"],
1467
1634
  },
1468
1635
  execute: async (_toolCallId, params) => {
1636
+ const locator = resolveToolLocator(ctx);
1637
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1638
+ if (!agentId) {
1639
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1640
+ }
1641
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1469
1642
  const { action, requestId, clock } = (params || {}) as {
1470
1643
  action?: unknown;
1471
1644
  requestId?: unknown;
@@ -1473,46 +1646,47 @@ const plugin = {
1473
1646
  };
1474
1647
 
1475
1648
  if (!isClockAction(action)) {
1476
- return {
1649
+ return jsonResult({
1477
1650
  success: false,
1478
1651
  error: "action must be one of: set, stop",
1479
- };
1652
+ });
1480
1653
  }
1481
1654
  if (requestId !== undefined && typeof requestId !== "string") {
1482
- return { success: false, error: "requestId must be a string when provided" };
1655
+ return jsonResult({ success: false, error: "requestId must be a string when provided" });
1483
1656
  }
1484
1657
  const normalizedRequestId = typeof requestId === "string" ? requestId : undefined;
1485
1658
  if (!service.hasValidIdentity() || !service.isConnected()) {
1486
- return { success: false, error: "Not connected to Kichi world" };
1659
+ return jsonResult({ success: false, error: "Not connected to Kichi world" });
1487
1660
  }
1488
1661
 
1489
1662
  let normalizedClock: ClockConfig | undefined;
1490
1663
  if (action === "set") {
1491
1664
  const { clock: nextClock, error } = normalizeClockConfig(clock);
1492
1665
  if (!nextClock) {
1493
- return { success: false, error: error ?? "Invalid clock payload" };
1666
+ return jsonResult({ success: false, error: error ?? "Invalid clock payload" });
1494
1667
  }
1495
1668
  normalizedClock = nextClock;
1496
1669
  }
1497
1670
 
1498
1671
  const sent = service.sendClock(action, normalizedClock, normalizedRequestId);
1499
1672
  if (!sent) {
1500
- return { success: false, error: "Failed to send clock payload" };
1673
+ return jsonResult({ success: false, error: "Failed to send clock payload" });
1501
1674
  }
1502
1675
 
1503
- return {
1676
+ return jsonResult({
1504
1677
  success: true,
1505
1678
  action,
1506
1679
  requestId: normalizedRequestId,
1507
1680
  ...(normalizedClock ? { clock: normalizedClock } : {}),
1508
- };
1681
+ });
1509
1682
  },
1510
- })));
1683
+ }), { name: "kichi_clock" });
1511
1684
 
1512
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1685
+ api.registerTool((ctx) => ({
1513
1686
  name: "kichi_query_status",
1687
+ label: "kichi_query_status",
1514
1688
  description:
1515
- "Query Kichi room and avatar status — includes room personnel, notes, ownerState, idlePlan, weather/time, timer snapshot, daily note quota, and `hasCreatedMusicAlbumToday`. Use this when the user asks to check kichi status, room status, or who is in the room. Also use this before creating a new note or daily recommended music album. For heartbeat planning, use the returned idlePlan as reference when shaping the next idle plan.",
1689
+ "Query Kichi room and avatar status — includes room personnel, notes, ownerState, idlePlan, weather/time, timer snapshot, daily note quota, `hasCreatedMusicAlbumToday`, and RoomContext.PoseableProps (poseable props with PropId, DisplayName, SupportedPoseTypes, OccupancyState). The PoseableProps list is cached internally so that kichi_action can reference a propId during regular work sync without re-querying. Use this when the user asks to check kichi status, room status, or who is in the room. Also use this before creating a new note or daily recommended music album. For heartbeat planning, use the returned idlePlan as reference when shaping the next idle plan.",
1516
1690
  parameters: {
1517
1691
  type: "object",
1518
1692
  properties: {
@@ -1523,30 +1697,37 @@ const plugin = {
1523
1697
  },
1524
1698
  },
1525
1699
  execute: async (_toolCallId, params) => {
1700
+ const locator = resolveToolLocator(ctx);
1701
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1702
+ if (!agentId) {
1703
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1704
+ }
1705
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1526
1706
  const requestId = (params as { requestId?: unknown } | null)?.requestId;
1527
1707
  if (requestId !== undefined && typeof requestId !== "string") {
1528
- return { success: false, error: "requestId must be a string when provided" };
1708
+ return jsonResult({ success: false, error: "requestId must be a string when provided" });
1529
1709
  }
1530
1710
  if (!service.hasValidIdentity() || !service.isConnected()) {
1531
- return { success: false, error: "Not connected to Kichi world" };
1711
+ return jsonResult({ success: false, error: "Not connected to Kichi world" });
1532
1712
  }
1533
1713
 
1534
1714
  try {
1535
1715
  const result = await service.queryStatus(
1536
1716
  typeof requestId === "string" ? requestId : undefined,
1537
1717
  );
1538
- return result;
1718
+ return jsonResult(result);
1539
1719
  } catch (error) {
1540
- return {
1720
+ return jsonResult({
1541
1721
  success: false,
1542
1722
  error: `Failed to query status: ${error}`,
1543
- };
1723
+ });
1544
1724
  }
1545
1725
  },
1546
- })));
1726
+ }), { name: "kichi_query_status" });
1547
1727
 
1548
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1728
+ api.registerTool((ctx) => ({
1549
1729
  name: "kichi_music_album_create",
1730
+ label: "kichi_music_album_create",
1550
1731
  description: buildMusicAlbumToolDescription(),
1551
1732
  parameters: {
1552
1733
  type: "object",
@@ -1571,6 +1752,12 @@ const plugin = {
1571
1752
  required: ["albumTitle", "musicTitles"],
1572
1753
  },
1573
1754
  execute: async (_toolCallId, params) => {
1755
+ const locator = resolveToolLocator(ctx);
1756
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1757
+ if (!agentId) {
1758
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1759
+ }
1760
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1574
1761
  const {
1575
1762
  requestId,
1576
1763
  albumTitle,
@@ -1582,33 +1769,33 @@ const plugin = {
1582
1769
  };
1583
1770
 
1584
1771
  if (requestId !== undefined && typeof requestId !== "string") {
1585
- return { success: false, error: "requestId must be a string when provided" };
1772
+ return jsonResult({ success: false, error: "requestId must be a string when provided" });
1586
1773
  }
1587
1774
  if (typeof albumTitle !== "string" || !albumTitle.trim()) {
1588
- return { success: false, error: "albumTitle is required" };
1775
+ return jsonResult({ success: false, error: "albumTitle is required" });
1589
1776
  }
1590
1777
  if (!Array.isArray(musicTitles)) {
1591
- return { success: false, error: "musicTitles must be an array of track names" };
1778
+ return jsonResult({ success: false, error: "musicTitles must be an array of track names" });
1592
1779
  }
1593
1780
 
1594
1781
  const { titles: normalizedTitles, invalidTitles } = normalizeMusicTitles(musicTitles);
1595
1782
  if (normalizedTitles.length === 0) {
1596
- return {
1783
+ return jsonResult({
1597
1784
  success: false,
1598
1785
  error: "musicTitles must contain at least one valid track name from the static config bundled with the plugin package",
1599
1786
  examples: getMusicTitleExamples(),
1600
- };
1787
+ });
1601
1788
  }
1602
1789
  if (invalidTitles.length > 0) {
1603
- return {
1790
+ return jsonResult({
1604
1791
  success: false,
1605
1792
  error: `Unknown musicTitles: ${invalidTitles.join(", ")}`,
1606
1793
  hint: "Use exact track names from the static config bundled with the plugin package",
1607
1794
  examples: getMusicTitleExamples(),
1608
- };
1795
+ });
1609
1796
  }
1610
1797
  if (!service.hasValidIdentity() || !service.isConnected()) {
1611
- return { success: false, error: "Not connected to Kichi world" };
1798
+ return jsonResult({ success: false, error: "Not connected to Kichi world" });
1612
1799
  }
1613
1800
 
1614
1801
  try {
@@ -1617,24 +1804,25 @@ const plugin = {
1617
1804
  normalizedTitles,
1618
1805
  typeof requestId === "string" ? requestId : undefined,
1619
1806
  );
1620
- return {
1807
+ return jsonResult({
1621
1808
  success: true,
1622
1809
  requestId: normalizedRequestId,
1623
1810
  albumTitle: albumTitle.trim(),
1624
1811
  musicTitles: normalizedTitles,
1625
1812
  trackCount: normalizedTitles.length,
1626
- };
1813
+ });
1627
1814
  } catch (error) {
1628
- return {
1815
+ return jsonResult({
1629
1816
  success: false,
1630
1817
  error: `Failed to create music album: ${error}`,
1631
- };
1818
+ });
1632
1819
  }
1633
1820
  },
1634
- })));
1821
+ }), { name: "kichi_music_album_create" });
1635
1822
 
1636
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1823
+ api.registerTool((ctx) => ({
1637
1824
  name: "kichi_noteboard_create",
1825
+ label: "kichi_noteboard_create",
1638
1826
  description:
1639
1827
  "Create a new note on a specific Kichi note board. Prefer querying first so you can avoid duplicate posts and respect rate limits.",
1640
1828
  parameters: {
@@ -1652,37 +1840,125 @@ const plugin = {
1652
1840
  required: ["propId", "data"],
1653
1841
  },
1654
1842
  execute: async (_toolCallId, params) => {
1843
+ const locator = resolveToolLocator(ctx);
1844
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1845
+ if (!agentId) {
1846
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1847
+ }
1848
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1655
1849
  const { propId, data } = (params || {}) as {
1656
1850
  propId?: unknown;
1657
1851
  data?: unknown;
1658
1852
  };
1659
1853
  if (typeof propId !== "string" || !propId.trim()) {
1660
- return { success: false, error: "propId is required" };
1854
+ return jsonResult({ success: false, error: "propId is required" });
1661
1855
  }
1662
1856
  if (typeof data !== "string" || !data.trim()) {
1663
- return { success: false, error: "data is required" };
1857
+ return jsonResult({ success: false, error: "data is required" });
1664
1858
  }
1665
1859
  if (data.trim().length > MAX_NOTEBOARD_TEXT_LENGTH) {
1666
- return {
1860
+ return jsonResult({
1667
1861
  success: false,
1668
1862
  error: `data must be ${MAX_NOTEBOARD_TEXT_LENGTH} characters or fewer`,
1669
- };
1863
+ });
1670
1864
  }
1671
1865
  if (!service.hasValidIdentity() || !service.isConnected()) {
1672
- return { success: false, error: "Not connected to Kichi world" };
1866
+ return jsonResult({ success: false, error: "Not connected to Kichi world" });
1673
1867
  }
1674
1868
 
1675
1869
  try {
1676
1870
  service.createNotesBoardNote(propId.trim(), data.trim());
1677
- return { success: true };
1871
+ return jsonResult({ success: true });
1678
1872
  } catch (error) {
1679
- return {
1873
+ return jsonResult({
1680
1874
  success: false,
1681
1875
  error: `Failed to create note: ${error}`,
1682
- };
1876
+ });
1877
+ }
1878
+ },
1879
+ }), { name: "kichi_noteboard_create" });
1880
+
1881
+ api.registerTool((ctx) => ({
1882
+ name: "kichi_bot_message",
1883
+ label: "kichi_bot_message",
1884
+ description:
1885
+ "Send a message to another bot in the same Kichi world. The bubble is the visible message content. Do not repeat what has already been said in the conversation history. When targeting a specific bot by name, call kichi_query_status first to resolve their avatarId. Only use \"*\" when broadcasting to all bots without a specific target.",
1886
+ parameters: {
1887
+ type: "object",
1888
+ properties: {
1889
+ toAvatarId: {
1890
+ type: "string",
1891
+ description: "Target bot's avatarId (resolve via kichi_query_status if unknown). Use \"*\" only for broadcasting to all bots.",
1892
+ },
1893
+ depth: {
1894
+ type: "number",
1895
+ description: "Conversation depth counter. Increment from the received message's depth.",
1896
+ },
1897
+ bubble: {
1898
+ type: "string",
1899
+ description: "The message to send (2-5 words, visible to everyone). Must not repeat previous messages.",
1900
+ },
1901
+ poseType: {
1902
+ type: "string",
1903
+ enum: ["stand", "sit", "lay", "floor"],
1904
+ description: "Optional pose change when sending.",
1905
+ },
1906
+ action: {
1907
+ type: "string",
1908
+ description: "Optional action to perform when sending.",
1909
+ },
1910
+ log: {
1911
+ type: "string",
1912
+ description: "Optional activity log entry.",
1913
+ },
1914
+ },
1915
+ required: ["toAvatarId", "depth", "bubble"],
1916
+ },
1917
+ execute: async (_toolCallId, params) => {
1918
+ const locator = resolveToolLocator(ctx);
1919
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1920
+ if (!agentId) {
1921
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1922
+ }
1923
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1924
+ const { toAvatarId, depth, bubble, poseType, action, log } = (params || {}) as {
1925
+ toAvatarId?: string;
1926
+ depth?: number;
1927
+ bubble?: string;
1928
+ poseType?: PoseType;
1929
+ action?: string;
1930
+ log?: string;
1931
+ };
1932
+ if (typeof toAvatarId !== "string" || !toAvatarId.trim()) {
1933
+ return jsonResult({ success: false, error: "toAvatarId is required" });
1934
+ }
1935
+ if (typeof depth !== "number" || depth < 0) {
1936
+ return jsonResult({ success: false, error: "depth must be a non-negative number" });
1937
+ }
1938
+ if (typeof bubble !== "string" || !bubble.trim()) {
1939
+ return jsonResult({ success: false, error: "bubble is required" });
1940
+ }
1941
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1942
+ return jsonResult({ success: false, error: "Not connected to Kichi world" });
1943
+ }
1944
+ try {
1945
+ let playback: ActionPlayback | undefined;
1946
+ if (poseType && action) {
1947
+ const actionDef = getActionDefinition(poseType, action);
1948
+ playback = getActionPlayback(actionDef);
1949
+ }
1950
+ const ack = await service.sendBotMessage(toAvatarId.trim(), depth, bubble.trim(), {
1951
+ poseType,
1952
+ action: action?.trim(),
1953
+ log: log?.trim(),
1954
+ playback,
1955
+ });
1956
+ return jsonResult({ success: true, ...ack });
1957
+ } catch (error) {
1958
+ return jsonResult({ success: false, error: `Failed to send bot message: ${error}` });
1683
1959
  }
1684
1960
  },
1685
- })));
1961
+ }), { name: "kichi_bot_message" });
1686
1962
 
1687
1963
  },
1688
1964
  };