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

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/dist/index.js CHANGED
@@ -193,7 +193,7 @@ function resolveEnvironmentHost(environment) {
193
193
  }
194
194
  function sendStatusUpdate(service, status) {
195
195
  const actionDefinition = getActionDefinition(status.poseType, status.action);
196
- service.sendStatus(status.poseType, actionDefinition.name, status.bubble || status.action, typeof status.log === "string" ? status.log.trim() : "", getActionPlayback(actionDefinition));
196
+ service.sendStatus(status.poseType, actionDefinition.name, status.bubble || status.action, typeof status.log === "string" ? status.log.trim() : "", getActionPlayback(actionDefinition), status.propId);
197
197
  }
198
198
  function syncFixedStatus(service, status) {
199
199
  if (!service.hasValidIdentity() || !service.isConnected()) {
@@ -557,6 +557,7 @@ function normalizeIdlePlan(value) {
557
557
  const actionDurationSeconds = rawAction.durationSeconds;
558
558
  const bubble = rawAction.bubble;
559
559
  const log = rawAction.log;
560
+ const propId = rawAction.propId;
560
561
  if (!["stand", "sit", "lay", "floor"].includes(String(poseType))) {
561
562
  return {
562
563
  error: `stages[${stageIndex}].actions[${actionIndex}].poseType must be stand, sit, lay, or floor`,
@@ -601,6 +602,7 @@ function normalizeIdlePlan(value) {
601
602
  durationSeconds: actionDurationSeconds,
602
603
  bubble: bubble.trim(),
603
604
  ...(typeof log === "string" && log.trim() ? { log: log.trim() } : {}),
605
+ ...(typeof propId === "string" && propId.trim() ? { propId: propId.trim() } : {}),
604
606
  });
605
607
  }
606
608
  if (stageActionDurationSeconds !== durationSeconds) {
@@ -797,9 +799,9 @@ function formatActionList(actions, playback) {
797
799
  .map((entry) => entry.name)
798
800
  .join(", ");
799
801
  }
800
- function buildKichiActionDescription() {
802
+ function buildKichiActionDescription(service) {
801
803
  const actions = loadStaticConfig().actions;
802
- return [
804
+ const lines = [
803
805
  "Directly control the avatar inside Kichi World.",
804
806
  "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.",
805
807
  "For most work, prefer a sit pose and switch actions as the task moves between stages.",
@@ -808,7 +810,13 @@ function buildKichiActionDescription() {
808
810
  `sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
809
811
  `lay actions: ${actions.lay.map((entry) => entry.name).join(", ")}`,
810
812
  `floor actions: ${actions.floor.map((entry) => entry.name).join(", ")}`,
811
- ].join("\n");
813
+ ];
814
+ const roomContext = service?.getCachedRoomContext();
815
+ const poseableProps = roomContext?.PoseableProps;
816
+ if (Array.isArray(poseableProps) && poseableProps.length > 0) {
817
+ lines.push("", "Cached RoomContext.PoseableProps (from last kichi_query_status):", JSON.stringify(poseableProps), "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.");
818
+ }
819
+ return lines.join("\n");
812
820
  }
813
821
  function buildKichiIdlePlanDescription() {
814
822
  const actions = loadStaticConfig().actions;
@@ -850,17 +858,6 @@ function buildKichiPrompt() {
850
858
  "User opt-out, Kichi config/test work, and explicit pose requests take priority over sync.",
851
859
  ].join("\n");
852
860
  }
853
- function createAgentScopedTool(runtimeManager, factory) {
854
- return (ctx) => {
855
- const locator = resolveToolLocator(ctx);
856
- const agentId = runtimeManager.resolveRuntimeAgentId(locator);
857
- if (!agentId) {
858
- throw new Error("Failed to resolve agent-scoped Kichi runtime");
859
- }
860
- const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
861
- return factory(service, ctx);
862
- };
863
- }
864
861
  const GLOBAL_RUNTIME_MANAGER_KEY = "__kichi_forwarder_runtime_manager__";
865
862
  function getRuntimeManager(logger) {
866
863
  const globalState = globalThis;
@@ -881,6 +878,11 @@ const plugin = {
881
878
  configSchema: { parse },
882
879
  register(api) {
883
880
  const runtimeManager = getRuntimeManager(api.logger);
881
+ runtimeManager.setEnvironmentHostResolver((environment) => {
882
+ const config = loadEnvironmentsConfig();
883
+ const host = config[environment];
884
+ return typeof host === "string" && host.trim() ? host : null;
885
+ });
884
886
  registerPluginHooks(api, runtimeManager);
885
887
  const musicTitleEnum = getMusicTitleEnum();
886
888
  runtimeManager.setBotMessageHandler((service, msg) => {
@@ -928,11 +930,6 @@ const plugin = {
928
930
  id: "kichi-forwarder",
929
931
  start: (ctx) => {
930
932
  parse(ctx.config.plugins?.entries?.["kichi-forwarder"]?.config);
931
- runtimeManager.setEnvironmentHostResolver((environment) => {
932
- const config = loadEnvironmentsConfig();
933
- const host = config[environment];
934
- return typeof host === "string" && host.trim() ? host : null;
935
- });
936
933
  runtimeManager.initializeStartupRuntimes();
937
934
  },
938
935
  stop: () => {
@@ -943,7 +940,7 @@ const plugin = {
943
940
  }
944
941
  },
945
942
  });
946
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
943
+ api.registerTool((ctx) => ({
947
944
  name: "kichi_join",
948
945
  label: "kichi_join",
949
946
  description: "Join Kichi world with avatarId, the current bot name, a short bio, and personality tags",
@@ -968,6 +965,12 @@ const plugin = {
968
965
  required: ["botName", "bio"],
969
966
  },
970
967
  execute: async (_toolCallId, params) => {
968
+ const locator = resolveToolLocator(ctx);
969
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
970
+ if (!agentId) {
971
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
972
+ }
973
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
971
974
  let avatarId = params?.avatarId;
972
975
  const botName = params?.botName?.trim();
973
976
  const bio = params?.bio?.trim();
@@ -999,54 +1002,58 @@ const plugin = {
999
1002
  ...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
1000
1003
  });
1001
1004
  },
1002
- })));
1003
- api.registerTool((ctx) => {
1004
- const locator = resolveToolLocator(ctx);
1005
- const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1006
- if (!agentId) {
1007
- throw new Error("Failed to resolve agent-scoped Kichi runtime");
1008
- }
1009
- const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1010
- return ({
1011
- name: "kichi_switch_host",
1012
- label: "kichi_switch_host",
1013
- description: "Switch Kichi runtime environment and reconnect immediately without restarting the gateway. Host is resolved from config/environments.json.",
1014
- parameters: {
1015
- type: "object",
1016
- properties: {
1017
- environment: {
1018
- type: "string",
1019
- enum: VALID_ENVIRONMENTS,
1020
- description: "Target environment: steam, steam-playtest, or test",
1021
- },
1005
+ }));
1006
+ api.registerTool((ctx) => ({
1007
+ name: "kichi_switch_host",
1008
+ label: "kichi_switch_host",
1009
+ description: "Switch Kichi runtime environment and reconnect immediately without restarting the gateway. Host is resolved from config/environments.json.",
1010
+ parameters: {
1011
+ type: "object",
1012
+ properties: {
1013
+ environment: {
1014
+ type: "string",
1015
+ enum: VALID_ENVIRONMENTS,
1016
+ description: "Target environment: steam, steam-playtest, or test",
1022
1017
  },
1023
- required: ["environment"],
1024
1018
  },
1025
- execute: async (_toolCallId, params) => {
1026
- const environment = params?.environment;
1027
- if (!isKichiEnvironment(environment)) {
1028
- return jsonResult({ success: false, error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` });
1029
- }
1030
- const resolved = resolveEnvironmentHost(environment);
1031
- if (resolved.error) {
1032
- return jsonResult({ success: false, error: resolved.error });
1033
- }
1034
- const status = await service.switchHost(resolved.host, environment);
1035
- return jsonResult({
1036
- success: true,
1037
- environment,
1038
- host: resolved.host,
1039
- status,
1040
- });
1041
- },
1042
- });
1043
- });
1044
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1019
+ required: ["environment"],
1020
+ },
1021
+ execute: async (_toolCallId, params) => {
1022
+ const locator = resolveToolLocator(ctx);
1023
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1024
+ if (!agentId) {
1025
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1026
+ }
1027
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1028
+ const environment = params?.environment;
1029
+ if (!isKichiEnvironment(environment)) {
1030
+ return jsonResult({ success: false, error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` });
1031
+ }
1032
+ const resolved = resolveEnvironmentHost(environment);
1033
+ if (resolved.error) {
1034
+ return jsonResult({ success: false, error: resolved.error });
1035
+ }
1036
+ const status = await service.switchHost(resolved.host, environment);
1037
+ return jsonResult({
1038
+ success: true,
1039
+ environment,
1040
+ host: resolved.host,
1041
+ status,
1042
+ });
1043
+ },
1044
+ }));
1045
+ api.registerTool((ctx) => ({
1045
1046
  name: "kichi_rejoin",
1046
1047
  label: "kichi_rejoin",
1047
1048
  description: "Request an immediate rejoin attempt with saved avatarId/authKey. Rejoin is also sent automatically after reconnect.",
1048
1049
  parameters: { type: "object", properties: {} },
1049
- execute: async () => {
1050
+ execute: async (_toolCallId, _params) => {
1051
+ const locator = resolveToolLocator(ctx);
1052
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1053
+ if (!agentId) {
1054
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1055
+ }
1056
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1050
1057
  const result = service.requestRejoin();
1051
1058
  return jsonResult({
1052
1059
  success: result.accepted,
@@ -1054,13 +1061,19 @@ const plugin = {
1054
1061
  status: service.getConnectionStatus(),
1055
1062
  });
1056
1063
  },
1057
- })));
1058
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1064
+ }));
1065
+ api.registerTool((ctx) => ({
1059
1066
  name: "kichi_leave",
1060
1067
  label: "kichi_leave",
1061
1068
  description: "Leave Kichi world",
1062
1069
  parameters: { type: "object", properties: {} },
1063
- execute: async () => {
1070
+ execute: async (_toolCallId, _params) => {
1071
+ const locator = resolveToolLocator(ctx);
1072
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1073
+ if (!agentId) {
1074
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1075
+ }
1076
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1064
1077
  const result = await service.leave();
1065
1078
  if (result.success) {
1066
1079
  return jsonResult({ success: true });
@@ -1073,105 +1086,126 @@ const plugin = {
1073
1086
  ...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
1074
1087
  });
1075
1088
  },
1076
- })));
1077
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1089
+ }));
1090
+ api.registerTool((ctx) => ({
1078
1091
  name: "kichi_connection_status",
1079
1092
  label: "kichi_connection_status",
1080
1093
  description: "Check WebSocket connection status and identity readiness only. Does NOT return room info, avatar state, or personnel — use kichi_query_status for that.",
1081
1094
  parameters: { type: "object", properties: {} },
1082
- execute: async () => {
1095
+ execute: async (_toolCallId, _params) => {
1096
+ const locator = resolveToolLocator(ctx);
1097
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1098
+ if (!agentId) {
1099
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1100
+ }
1101
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1083
1102
  return jsonResult({
1084
1103
  success: true,
1085
1104
  status: service.getConnectionStatus(),
1086
1105
  });
1087
1106
  },
1088
- })));
1089
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1090
- name: "kichi_action",
1091
- label: "kichi_action",
1092
- description: buildKichiActionDescription(),
1093
- parameters: {
1094
- type: "object",
1095
- properties: {
1096
- poseType: { type: "string", description: "Pose type: stand, sit, lay, or floor" },
1097
- action: {
1098
- type: "string",
1099
- description: "Action name for the selected pose (for example Sit Nicely, Typing with Keyboard, Reading, High Five, or Meditate)",
1100
- },
1101
- bubble: { type: "string", description: "Optional bubble text to display (max 5 words)" },
1102
- log: {
1103
- type: "string",
1104
- description: "Short natural first-person sentence under 15 words. Match the language of the bubble and mention the current action and immediate focus.",
1105
- },
1106
- verify: {
1107
- type: "boolean",
1108
- description: "Set true ONLY when the user explicitly requests a pose or action. Omit during routine sync steps.",
1107
+ }));
1108
+ api.registerTool((ctx) => {
1109
+ const locator = resolveToolLocator(ctx);
1110
+ const existingService = runtimeManager.getRuntime(locator);
1111
+ return ({
1112
+ name: "kichi_action",
1113
+ label: "kichi_action",
1114
+ description: buildKichiActionDescription(existingService ?? undefined),
1115
+ parameters: {
1116
+ type: "object",
1117
+ properties: {
1118
+ poseType: { type: "string", description: "Pose type: stand, sit, lay, or floor" },
1119
+ action: {
1120
+ type: "string",
1121
+ description: "Action name for the selected pose (for example Sit Nicely, Typing with Keyboard, Reading, High Five, or Meditate)",
1122
+ },
1123
+ bubble: { type: "string", description: "Optional bubble text to display (max 5 words)" },
1124
+ log: {
1125
+ type: "string",
1126
+ description: "Short natural first-person sentence under 15 words. Match the language of the bubble and mention the current action and immediate focus.",
1127
+ },
1128
+ verify: {
1129
+ type: "boolean",
1130
+ description: "Set true ONLY when the user explicitly requests a pose or action. Omit during routine sync steps.",
1131
+ },
1132
+ propId: {
1133
+ type: "string",
1134
+ description: "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.",
1135
+ },
1109
1136
  },
1137
+ required: ["poseType", "action"],
1110
1138
  },
1111
- required: ["poseType", "action"],
1112
- },
1113
- execute: async (_toolCallId, params) => {
1114
- const { poseType, action, bubble, log, verify } = (params || {});
1115
- if (!poseType || !action) {
1116
- return jsonResult({ success: false, error: "poseType and action parameters are required" });
1117
- }
1118
- if (!["stand", "sit", "lay", "floor"].includes(poseType)) {
1119
- return jsonResult({
1120
- success: false,
1121
- error: `Invalid poseType: ${poseType}. Must be stand, sit, lay, or floor`,
1122
- });
1123
- }
1124
- if (!service.hasValidIdentity() || !service.isConnected()) {
1125
- return jsonResult({ success: false, error: "Not connected to Kichi world" });
1126
- }
1127
- const normalizedPoseType = poseType;
1128
- const poseActions = loadStaticConfig().actions[normalizedPoseType];
1129
- const matched = poseActions.find((entry) => entry.name.toLowerCase() === action.toLowerCase());
1130
- if (!matched) {
1131
- return jsonResult({
1132
- success: false,
1133
- error: `Unknown action "${action}" for poseType "${poseType}"`,
1134
- available: poseActions.map((entry) => entry.name),
1135
- });
1136
- }
1137
- const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : matched.name;
1138
- const logText = typeof log === "string" ? log.trim() : "";
1139
- const playback = getActionPlayback(matched);
1140
- if (verify) {
1141
- try {
1142
- const ack = await service.sendStatusVerified(normalizedPoseType, matched.name, bubbleText, logText, playback);
1143
- if (ack.warning) {
1144
- return jsonResult({
1145
- success: true,
1146
- requested: { poseType: normalizedPoseType, action: matched.name },
1147
- actual: { poseType: ack.poseType, action: ack.action },
1148
- warning: ack.warning,
1149
- });
1139
+ execute: async (_toolCallId, params) => {
1140
+ const locator = resolveToolLocator(ctx);
1141
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1142
+ if (!agentId) {
1143
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1144
+ }
1145
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1146
+ const { poseType, action, bubble, log, verify, propId } = (params || {});
1147
+ if (!poseType || !action) {
1148
+ return jsonResult({ success: false, error: "poseType and action parameters are required" });
1149
+ }
1150
+ if (!["stand", "sit", "lay", "floor"].includes(poseType)) {
1151
+ return jsonResult({
1152
+ success: false,
1153
+ error: `Invalid poseType: ${poseType}. Must be stand, sit, lay, or floor`,
1154
+ });
1155
+ }
1156
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1157
+ return jsonResult({ success: false, error: "Not connected to Kichi world" });
1158
+ }
1159
+ const normalizedPoseType = poseType;
1160
+ const poseActions = loadStaticConfig().actions[normalizedPoseType];
1161
+ const matched = poseActions.find((entry) => entry.name.toLowerCase() === action.toLowerCase());
1162
+ if (!matched) {
1163
+ return jsonResult({
1164
+ success: false,
1165
+ error: `Unknown action "${action}" for poseType "${poseType}"`,
1166
+ available: poseActions.map((entry) => entry.name),
1167
+ });
1168
+ }
1169
+ const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : matched.name;
1170
+ const logText = typeof log === "string" ? log.trim() : "";
1171
+ const playback = getActionPlayback(matched);
1172
+ if (verify) {
1173
+ try {
1174
+ const ack = await service.sendStatusVerified(normalizedPoseType, matched.name, bubbleText, logText, playback, propId);
1175
+ if (ack.warning) {
1176
+ return jsonResult({
1177
+ success: true,
1178
+ requested: { poseType: normalizedPoseType, action: matched.name },
1179
+ actual: { poseType: ack.poseType, action: ack.action },
1180
+ warning: ack.warning,
1181
+ });
1182
+ }
1183
+ }
1184
+ catch {
1185
+ // Server not updated or timeout — fall through to normal success
1150
1186
  }
1151
1187
  }
1152
- catch {
1153
- // Server not updated or timeout — fall through to normal success
1188
+ else {
1189
+ sendStatusUpdate(service, {
1190
+ poseType: normalizedPoseType,
1191
+ action: matched.name,
1192
+ bubble: bubbleText,
1193
+ log: logText,
1194
+ propId,
1195
+ });
1154
1196
  }
1155
- }
1156
- else {
1157
- sendStatusUpdate(service, {
1197
+ return jsonResult({
1198
+ success: true,
1158
1199
  poseType: normalizedPoseType,
1159
1200
  action: matched.name,
1160
1201
  bubble: bubbleText,
1161
1202
  log: logText,
1203
+ playback,
1162
1204
  });
1163
- }
1164
- return jsonResult({
1165
- success: true,
1166
- poseType: normalizedPoseType,
1167
- action: matched.name,
1168
- bubble: bubbleText,
1169
- log: logText,
1170
- playback,
1171
- });
1172
- },
1173
- })));
1174
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1205
+ },
1206
+ });
1207
+ });
1208
+ api.registerTool((ctx) => ({
1175
1209
  name: "kichi_idle_plan",
1176
1210
  label: "kichi_idle_plan",
1177
1211
  description: buildKichiIdlePlanDescription(),
@@ -1239,6 +1273,10 @@ const plugin = {
1239
1273
  type: "string",
1240
1274
  description: "Optional log content for this action. Use the same language as the current conversation.",
1241
1275
  },
1276
+ propId: {
1277
+ type: "string",
1278
+ description: "Optional poseable prop ID from RoomContext.PoseableProps. When specified, the avatar is seated at this prop.",
1279
+ },
1242
1280
  },
1243
1281
  required: ["poseType", "action", "durationSeconds", "bubble"],
1244
1282
  },
@@ -1251,6 +1289,12 @@ const plugin = {
1251
1289
  required: ["heartbeatIntervalSeconds", "goal", "stages"],
1252
1290
  },
1253
1291
  execute: async (_toolCallId, params) => {
1292
+ const locator = resolveToolLocator(ctx);
1293
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1294
+ if (!agentId) {
1295
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1296
+ }
1297
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1254
1298
  const { idlePlan, error } = normalizeIdlePlan(params);
1255
1299
  if (!idlePlan) {
1256
1300
  return jsonResult({ success: false, error: error ?? "Invalid idle plan payload" });
@@ -1276,8 +1320,8 @@ const plugin = {
1276
1320
  stages: idlePlan.stages,
1277
1321
  });
1278
1322
  },
1279
- })));
1280
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1323
+ }));
1324
+ api.registerTool((ctx) => ({
1281
1325
  name: "kichi_clock",
1282
1326
  label: "kichi_clock",
1283
1327
  description: "Send clock commands to Kichi world. Supported actions are set and stop.",
@@ -1346,6 +1390,12 @@ const plugin = {
1346
1390
  required: ["action"],
1347
1391
  },
1348
1392
  execute: async (_toolCallId, params) => {
1393
+ const locator = resolveToolLocator(ctx);
1394
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1395
+ if (!agentId) {
1396
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1397
+ }
1398
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1349
1399
  const { action, requestId, clock } = (params || {});
1350
1400
  if (!isClockAction(action)) {
1351
1401
  return jsonResult({
@@ -1379,11 +1429,11 @@ const plugin = {
1379
1429
  ...(normalizedClock ? { clock: normalizedClock } : {}),
1380
1430
  });
1381
1431
  },
1382
- })));
1383
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1432
+ }));
1433
+ api.registerTool((ctx) => ({
1384
1434
  name: "kichi_query_status",
1385
1435
  label: "kichi_query_status",
1386
- description: "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.",
1436
+ description: "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.",
1387
1437
  parameters: {
1388
1438
  type: "object",
1389
1439
  properties: {
@@ -1394,6 +1444,12 @@ const plugin = {
1394
1444
  },
1395
1445
  },
1396
1446
  execute: async (_toolCallId, params) => {
1447
+ const locator = resolveToolLocator(ctx);
1448
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1449
+ if (!agentId) {
1450
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1451
+ }
1452
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1397
1453
  const requestId = params?.requestId;
1398
1454
  if (requestId !== undefined && typeof requestId !== "string") {
1399
1455
  return jsonResult({ success: false, error: "requestId must be a string when provided" });
@@ -1412,8 +1468,8 @@ const plugin = {
1412
1468
  });
1413
1469
  }
1414
1470
  },
1415
- })));
1416
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1471
+ }));
1472
+ api.registerTool((ctx) => ({
1417
1473
  name: "kichi_music_album_create",
1418
1474
  label: "kichi_music_album_create",
1419
1475
  description: buildMusicAlbumToolDescription(),
@@ -1440,6 +1496,12 @@ const plugin = {
1440
1496
  required: ["albumTitle", "musicTitles"],
1441
1497
  },
1442
1498
  execute: async (_toolCallId, params) => {
1499
+ const locator = resolveToolLocator(ctx);
1500
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1501
+ if (!agentId) {
1502
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1503
+ }
1504
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1443
1505
  const { requestId, albumTitle, musicTitles, } = (params || {});
1444
1506
  if (requestId !== undefined && typeof requestId !== "string") {
1445
1507
  return jsonResult({ success: false, error: "requestId must be a string when provided" });
@@ -1486,8 +1548,8 @@ const plugin = {
1486
1548
  });
1487
1549
  }
1488
1550
  },
1489
- })));
1490
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1551
+ }));
1552
+ api.registerTool((ctx) => ({
1491
1553
  name: "kichi_noteboard_create",
1492
1554
  label: "kichi_noteboard_create",
1493
1555
  description: "Create a new note on a specific Kichi note board. Prefer querying first so you can avoid duplicate posts and respect rate limits.",
@@ -1506,6 +1568,12 @@ const plugin = {
1506
1568
  required: ["propId", "data"],
1507
1569
  },
1508
1570
  execute: async (_toolCallId, params) => {
1571
+ const locator = resolveToolLocator(ctx);
1572
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1573
+ if (!agentId) {
1574
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1575
+ }
1576
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1509
1577
  const { propId, data } = (params || {});
1510
1578
  if (typeof propId !== "string" || !propId.trim()) {
1511
1579
  return jsonResult({ success: false, error: "propId is required" });
@@ -1533,8 +1601,8 @@ const plugin = {
1533
1601
  });
1534
1602
  }
1535
1603
  },
1536
- })));
1537
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1604
+ }));
1605
+ api.registerTool((ctx) => ({
1538
1606
  name: "kichi_bot_message",
1539
1607
  label: "kichi_bot_message",
1540
1608
  description: "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.",
@@ -1570,6 +1638,12 @@ const plugin = {
1570
1638
  required: ["toAvatarId", "depth", "bubble"],
1571
1639
  },
1572
1640
  execute: async (_toolCallId, params) => {
1641
+ const locator = resolveToolLocator(ctx);
1642
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1643
+ if (!agentId) {
1644
+ return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1645
+ }
1646
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1573
1647
  const { toAvatarId, depth, bubble, poseType, action, log } = (params || {});
1574
1648
  if (typeof toAvatarId !== "string" || !toAvatarId.trim()) {
1575
1649
  return jsonResult({ success: false, error: "toAvatarId is required" });
@@ -1601,7 +1675,7 @@ const plugin = {
1601
1675
  return jsonResult({ success: false, error: `Failed to send bot message: ${error}` });
1602
1676
  }
1603
1677
  },
1604
- })));
1678
+ }));
1605
1679
  },
1606
1680
  };
1607
1681
  export default plugin;
@@ -17,6 +17,7 @@ export class KichiForwarderService {
17
17
  joinResolve = null;
18
18
  pendingRequests = new Map();
19
19
  onBotMessageReceived = null;
20
+ cachedRoomContext = null;
20
21
  constructor(logger, options) {
21
22
  this.logger = logger;
22
23
  this.options = options;
@@ -91,7 +92,7 @@ export class KichiForwarderService {
91
92
  }, 10000);
92
93
  });
93
94
  }
94
- sendStatus(poseType, action, bubble, log, playback) {
95
+ sendStatus(poseType, action, bubble, log, playback, propId) {
95
96
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN)
96
97
  return;
97
98
  const payload = {
@@ -103,10 +104,11 @@ export class KichiForwarderService {
103
104
  bubble,
104
105
  log,
105
106
  playback,
107
+ ...(propId ? { propId } : {}),
106
108
  };
107
109
  this.ws.send(JSON.stringify(payload));
108
110
  }
109
- async sendStatusVerified(poseType, action, bubble, log, playback) {
111
+ async sendStatusVerified(poseType, action, bubble, log, playback, propId) {
110
112
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) {
111
113
  throw new Error("Kichi websocket is not connected");
112
114
  }
@@ -120,6 +122,7 @@ export class KichiForwarderService {
120
122
  bubble,
121
123
  log,
122
124
  playback,
125
+ ...(propId ? { propId } : {}),
123
126
  };
124
127
  return this.sendRequest(payload, "status_ack", 5000);
125
128
  }
@@ -181,7 +184,11 @@ export class KichiForwarderService {
181
184
  avatarId: identity.avatarId,
182
185
  authKey: identity.authKey,
183
186
  };
184
- return this.sendRequest(payload, "query_status_result");
187
+ const result = await this.sendRequest(payload, "query_status_result");
188
+ if (result.RoomContext && typeof result.RoomContext === "object") {
189
+ this.cachedRoomContext = result.RoomContext;
190
+ }
191
+ return result;
185
192
  }
186
193
  createNotesBoardNote(propId, data) {
187
194
  const identity = this.requireIdentity();
@@ -250,6 +257,7 @@ export class KichiForwarderService {
250
257
  return this.sendRequest(payload, "bot_message_ack", 5000);
251
258
  }
252
259
  isConnected() { return this.ws?.readyState === WebSocket.OPEN && !!this.identity?.authKey; }
260
+ getCachedRoomContext() { return this.cachedRoomContext; }
253
261
  hasValidIdentity() { return !!this.identity?.avatarId && !!this.identity?.authKey; }
254
262
  isLlmRuntimeEnabled() {
255
263
  return this.readStateFile()?.llmRuntimeEnabled ?? DEFAULT_LLM_RUNTIME_ENABLED;
package/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import type {
4
- AnyAgentTool,
5
4
  OpenClawPluginApi,
6
5
  } from "openclaw/plugin-sdk";
7
6
  import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
@@ -264,6 +263,7 @@ function sendStatusUpdate(service: KichiForwarderService, status: ActionResult):
264
263
  status.bubble || status.action,
265
264
  typeof status.log === "string" ? status.log.trim() : "",
266
265
  getActionPlayback(actionDefinition),
266
+ status.propId,
267
267
  );
268
268
  }
269
269
 
@@ -712,6 +712,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
712
712
  const actionDurationSeconds = rawAction.durationSeconds;
713
713
  const bubble = rawAction.bubble;
714
714
  const log = rawAction.log;
715
+ const propId = rawAction.propId;
715
716
 
716
717
  if (!["stand", "sit", "lay", "floor"].includes(String(poseType))) {
717
718
  return {
@@ -759,6 +760,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
759
760
  durationSeconds: actionDurationSeconds,
760
761
  bubble: bubble.trim(),
761
762
  ...(typeof log === "string" && log.trim() ? { log: log.trim() } : {}),
763
+ ...(typeof propId === "string" && propId.trim() ? { propId: propId.trim() } : {}),
762
764
  });
763
765
  }
764
766
 
@@ -992,9 +994,9 @@ function formatActionList(actions: ActionDefinition[], playback: ActionPlayback[
992
994
  .join(", ");
993
995
  }
994
996
 
995
- function buildKichiActionDescription(): string {
997
+ function buildKichiActionDescription(service?: KichiForwarderService): string {
996
998
  const actions = loadStaticConfig().actions;
997
- return [
999
+ const lines = [
998
1000
  "Directly control the avatar inside Kichi World.",
999
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.",
1000
1002
  "For most work, prefer a sit pose and switch actions as the task moves between stages.",
@@ -1003,7 +1005,20 @@ function buildKichiActionDescription(): string {
1003
1005
  `sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
1004
1006
  `lay actions: ${actions.lay.map((entry) => entry.name).join(", ")}`,
1005
1007
  `floor actions: ${actions.floor.map((entry) => entry.name).join(", ")}`,
1006
- ].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");
1007
1022
  }
1008
1023
 
1009
1024
  function buildKichiIdlePlanDescription(): string {
@@ -1048,20 +1063,6 @@ function buildKichiPrompt(): string {
1048
1063
  ].join("\n");
1049
1064
  }
1050
1065
 
1051
- function createAgentScopedTool(
1052
- runtimeManager: KichiRuntimeManager,
1053
- factory: (service: KichiForwarderService, ctx: OpenClawPluginToolContext) => AnyAgentTool,
1054
- ) {
1055
- return (ctx: OpenClawPluginToolContext) => {
1056
- const locator = resolveToolLocator(ctx);
1057
- const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1058
- if (!agentId) {
1059
- throw new Error("Failed to resolve agent-scoped Kichi runtime");
1060
- }
1061
- const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1062
- return factory(service, ctx);
1063
- };
1064
- }
1065
1066
 
1066
1067
  const GLOBAL_RUNTIME_MANAGER_KEY = "__kichi_forwarder_runtime_manager__";
1067
1068
 
@@ -1091,6 +1092,13 @@ const plugin = {
1091
1092
 
1092
1093
  register(api: OpenClawPluginApi) {
1093
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
+
1094
1102
  registerPluginHooks(api, runtimeManager);
1095
1103
  const musicTitleEnum = getMusicTitleEnum();
1096
1104
 
@@ -1139,11 +1147,6 @@ const plugin = {
1139
1147
  id: "kichi-forwarder",
1140
1148
  start: (ctx) => {
1141
1149
  parse(ctx.config.plugins?.entries?.["kichi-forwarder"]?.config);
1142
- runtimeManager.setEnvironmentHostResolver((environment) => {
1143
- const config = loadEnvironmentsConfig();
1144
- const host = config[environment];
1145
- return typeof host === "string" && host.trim() ? host : null;
1146
- });
1147
1150
  runtimeManager.initializeStartupRuntimes();
1148
1151
  },
1149
1152
  stop: () => {
@@ -1155,7 +1158,7 @@ const plugin = {
1155
1158
  },
1156
1159
  });
1157
1160
 
1158
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1161
+ api.registerTool((ctx) => ({
1159
1162
  name: "kichi_join",
1160
1163
  label: "kichi_join",
1161
1164
  description: "Join Kichi world with avatarId, the current bot name, a short bio, and personality tags",
@@ -1180,6 +1183,12 @@ const plugin = {
1180
1183
  required: ["botName", "bio"],
1181
1184
  },
1182
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);
1183
1192
  let avatarId = (params as { avatarId?: string } | null)?.avatarId;
1184
1193
  const botName = (params as { botName?: string } | null)?.botName?.trim();
1185
1194
  const bio = (params as { bio?: string } | null)?.bio?.trim();
@@ -1213,16 +1222,9 @@ const plugin = {
1213
1222
  ...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
1214
1223
  });
1215
1224
  },
1216
- })));
1225
+ }));
1217
1226
 
1218
- api.registerTool((ctx: OpenClawPluginToolContext) => {
1219
- const locator = resolveToolLocator(ctx);
1220
- const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1221
- if (!agentId) {
1222
- throw new Error("Failed to resolve agent-scoped Kichi runtime");
1223
- }
1224
- const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1225
- return ({
1227
+ api.registerTool((ctx) => ({
1226
1228
  name: "kichi_switch_host",
1227
1229
  label: "kichi_switch_host",
1228
1230
  description:
@@ -1239,6 +1241,12 @@ const plugin = {
1239
1241
  required: ["environment"],
1240
1242
  },
1241
1243
  execute: async (_toolCallId, params) => {
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);
1242
1250
  const environment = (params as { environment?: unknown } | null)?.environment;
1243
1251
  if (!isKichiEnvironment(environment)) {
1244
1252
  return jsonResult({ success: false, error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` });
@@ -1257,16 +1265,21 @@ const plugin = {
1257
1265
  status,
1258
1266
  });
1259
1267
  },
1260
- });
1261
- });
1268
+ }));
1262
1269
 
1263
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1270
+ api.registerTool((ctx) => ({
1264
1271
  name: "kichi_rejoin",
1265
1272
  label: "kichi_rejoin",
1266
1273
  description:
1267
1274
  "Request an immediate rejoin attempt with saved avatarId/authKey. Rejoin is also sent automatically after reconnect.",
1268
1275
  parameters: { type: "object", properties: {} },
1269
- 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);
1270
1283
  const result = service.requestRejoin();
1271
1284
  return jsonResult({
1272
1285
  success: result.accepted,
@@ -1274,14 +1287,20 @@ const plugin = {
1274
1287
  status: service.getConnectionStatus(),
1275
1288
  });
1276
1289
  },
1277
- })));
1290
+ }));
1278
1291
 
1279
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1292
+ api.registerTool((ctx) => ({
1280
1293
  name: "kichi_leave",
1281
1294
  label: "kichi_leave",
1282
1295
  description: "Leave Kichi world",
1283
1296
  parameters: { type: "object", properties: {} },
1284
- 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);
1285
1304
  const result = await service.leave();
1286
1305
  if (result.success) {
1287
1306
  return jsonResult({ success: true });
@@ -1294,25 +1313,34 @@ const plugin = {
1294
1313
  ...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
1295
1314
  });
1296
1315
  },
1297
- })));
1316
+ }));
1298
1317
 
1299
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1318
+ api.registerTool((ctx) => ({
1300
1319
  name: "kichi_connection_status",
1301
1320
  label: "kichi_connection_status",
1302
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.",
1303
1322
  parameters: { type: "object", properties: {} },
1304
- execute: async () => {
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);
1305
1330
  return jsonResult({
1306
1331
  success: true,
1307
1332
  status: service.getConnectionStatus(),
1308
1333
  });
1309
1334
  },
1310
- })));
1335
+ }));
1311
1336
 
1312
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1337
+ api.registerTool((ctx) => {
1338
+ const locator = resolveToolLocator(ctx);
1339
+ const existingService = runtimeManager.getRuntime(locator);
1340
+ return ({
1313
1341
  name: "kichi_action",
1314
1342
  label: "kichi_action",
1315
- description: buildKichiActionDescription(),
1343
+ description: buildKichiActionDescription(existingService ?? undefined),
1316
1344
  parameters: {
1317
1345
  type: "object",
1318
1346
  properties: {
@@ -1332,16 +1360,28 @@ const plugin = {
1332
1360
  description:
1333
1361
  "Set true ONLY when the user explicitly requests a pose or action. Omit during routine sync steps.",
1334
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
+ },
1335
1368
  },
1336
1369
  required: ["poseType", "action"],
1337
1370
  },
1338
1371
  execute: async (_toolCallId, params) => {
1339
- 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 {
1340
1379
  poseType?: string;
1341
1380
  action?: string;
1342
1381
  bubble?: string;
1343
1382
  log?: string;
1344
1383
  verify?: boolean;
1384
+ propId?: string;
1345
1385
  };
1346
1386
  if (!poseType || !action) {
1347
1387
  return jsonResult({ success: false, error: "poseType and action parameters are required" });
@@ -1374,7 +1414,7 @@ const plugin = {
1374
1414
  if (verify) {
1375
1415
  try {
1376
1416
  const ack = await service.sendStatusVerified(
1377
- normalizedPoseType, matched.name, bubbleText, logText, playback,
1417
+ normalizedPoseType, matched.name, bubbleText, logText, playback, propId,
1378
1418
  );
1379
1419
  if (ack.warning) {
1380
1420
  return jsonResult({
@@ -1393,6 +1433,7 @@ const plugin = {
1393
1433
  action: matched.name,
1394
1434
  bubble: bubbleText,
1395
1435
  log: logText,
1436
+ propId,
1396
1437
  });
1397
1438
  }
1398
1439
 
@@ -1405,8 +1446,8 @@ const plugin = {
1405
1446
  playback,
1406
1447
  });
1407
1448
  },
1408
- })));
1409
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1449
+ })});
1450
+ api.registerTool((ctx) => ({
1410
1451
  name: "kichi_idle_plan",
1411
1452
  label: "kichi_idle_plan",
1412
1453
  description: buildKichiIdlePlanDescription(),
@@ -1474,6 +1515,10 @@ const plugin = {
1474
1515
  type: "string",
1475
1516
  description: "Optional log content for this action. Use the same language as the current conversation.",
1476
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
+ },
1477
1522
  },
1478
1523
  required: ["poseType", "action", "durationSeconds", "bubble"],
1479
1524
  },
@@ -1486,6 +1531,12 @@ const plugin = {
1486
1531
  required: ["heartbeatIntervalSeconds", "goal", "stages"],
1487
1532
  },
1488
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);
1489
1540
  const { idlePlan, error } = normalizeIdlePlan(params);
1490
1541
  if (!idlePlan) {
1491
1542
  return jsonResult({ success: false, error: error ?? "Invalid idle plan payload" });
@@ -1511,8 +1562,8 @@ const plugin = {
1511
1562
  stages: idlePlan.stages,
1512
1563
  });
1513
1564
  },
1514
- })));
1515
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1565
+ }));
1566
+ api.registerTool((ctx) => ({
1516
1567
  name: "kichi_clock",
1517
1568
  label: "kichi_clock",
1518
1569
  description:
@@ -1582,6 +1633,12 @@ const plugin = {
1582
1633
  required: ["action"],
1583
1634
  },
1584
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);
1585
1642
  const { action, requestId, clock } = (params || {}) as {
1586
1643
  action?: unknown;
1587
1644
  requestId?: unknown;
@@ -1623,13 +1680,13 @@ const plugin = {
1623
1680
  ...(normalizedClock ? { clock: normalizedClock } : {}),
1624
1681
  });
1625
1682
  },
1626
- })));
1683
+ }));
1627
1684
 
1628
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1685
+ api.registerTool((ctx) => ({
1629
1686
  name: "kichi_query_status",
1630
1687
  label: "kichi_query_status",
1631
1688
  description:
1632
- "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.",
1633
1690
  parameters: {
1634
1691
  type: "object",
1635
1692
  properties: {
@@ -1640,6 +1697,12 @@ const plugin = {
1640
1697
  },
1641
1698
  },
1642
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);
1643
1706
  const requestId = (params as { requestId?: unknown } | null)?.requestId;
1644
1707
  if (requestId !== undefined && typeof requestId !== "string") {
1645
1708
  return jsonResult({ success: false, error: "requestId must be a string when provided" });
@@ -1660,9 +1723,9 @@ const plugin = {
1660
1723
  });
1661
1724
  }
1662
1725
  },
1663
- })));
1726
+ }));
1664
1727
 
1665
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1728
+ api.registerTool((ctx) => ({
1666
1729
  name: "kichi_music_album_create",
1667
1730
  label: "kichi_music_album_create",
1668
1731
  description: buildMusicAlbumToolDescription(),
@@ -1689,6 +1752,12 @@ const plugin = {
1689
1752
  required: ["albumTitle", "musicTitles"],
1690
1753
  },
1691
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);
1692
1761
  const {
1693
1762
  requestId,
1694
1763
  albumTitle,
@@ -1749,9 +1818,9 @@ const plugin = {
1749
1818
  });
1750
1819
  }
1751
1820
  },
1752
- })));
1821
+ }));
1753
1822
 
1754
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1823
+ api.registerTool((ctx) => ({
1755
1824
  name: "kichi_noteboard_create",
1756
1825
  label: "kichi_noteboard_create",
1757
1826
  description:
@@ -1771,6 +1840,12 @@ const plugin = {
1771
1840
  required: ["propId", "data"],
1772
1841
  },
1773
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);
1774
1849
  const { propId, data } = (params || {}) as {
1775
1850
  propId?: unknown;
1776
1851
  data?: unknown;
@@ -1801,9 +1876,9 @@ const plugin = {
1801
1876
  });
1802
1877
  }
1803
1878
  },
1804
- })));
1879
+ }));
1805
1880
 
1806
- api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1881
+ api.registerTool((ctx) => ({
1807
1882
  name: "kichi_bot_message",
1808
1883
  label: "kichi_bot_message",
1809
1884
  description:
@@ -1840,6 +1915,12 @@ const plugin = {
1840
1915
  required: ["toAvatarId", "depth", "bubble"],
1841
1916
  },
1842
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);
1843
1924
  const { toAvatarId, depth, bubble, poseType, action, log } = (params || {}) as {
1844
1925
  toAvatarId?: string;
1845
1926
  depth?: number;
@@ -1877,7 +1958,7 @@ const plugin = {
1877
1958
  return jsonResult({ success: false, error: `Failed to send bot message: ${error}` });
1878
1959
  }
1879
1960
  },
1880
- })));
1961
+ }));
1881
1962
 
1882
1963
  },
1883
1964
  };
@@ -2,9 +2,25 @@
2
2
  "id": "kichi-forwarder",
3
3
  "name": "Kichi Forwarder",
4
4
  "description": "Native OpenClaw plugin for Kichi World with direct avatar control, status sync, timers, notes, and music tools",
5
- "version": "0.1.2-beta.7",
5
+ "version": "0.1.2-beta.9",
6
6
  "author": "OpenClaw",
7
7
  "skills": ["./skills/kichi-forwarder"],
8
+ "contracts": {
9
+ "tools": [
10
+ "kichi_join",
11
+ "kichi_switch_host",
12
+ "kichi_rejoin",
13
+ "kichi_leave",
14
+ "kichi_connection_status",
15
+ "kichi_action",
16
+ "kichi_idle_plan",
17
+ "kichi_clock",
18
+ "kichi_query_status",
19
+ "kichi_music_album_create",
20
+ "kichi_noteboard_create",
21
+ "kichi_bot_message"
22
+ ]
23
+ },
8
24
  "configSchema": {
9
25
  "type": "object",
10
26
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yahaha-studio/kichi-forwarder",
3
- "version": "0.1.2-beta.7",
3
+ "version": "0.1.2-beta.9",
4
4
  "description": "Native OpenClaw plugin for Kichi World with direct avatar control, status sync, timers, notes, and music tools",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,12 +22,12 @@
22
22
  "./dist/index.js"
23
23
  ],
24
24
  "compat": {
25
- "pluginApi": ">=2026.3.24",
26
- "minGatewayVersion": "2026.3.24"
25
+ "pluginApi": ">=2026.4.25",
26
+ "minGatewayVersion": "2026.4.25"
27
27
  },
28
28
  "build": {
29
- "openclawVersion": "2026.3.24",
30
- "pluginSdkVersion": "2026.3.24"
29
+ "openclawVersion": "2026.5.7",
30
+ "pluginSdkVersion": "2026.5.7"
31
31
  }
32
32
  },
33
33
  "scripts": {
@@ -43,7 +43,7 @@
43
43
  "devDependencies": {
44
44
  "@types/node": "^24.3.0",
45
45
  "@types/ws": "^8.18.1",
46
- "openclaw": "2026.3.24",
46
+ "openclaw": "2026.5.7",
47
47
  "typescript": "^6.0.3"
48
48
  }
49
49
  }
package/src/service.ts CHANGED
@@ -83,6 +83,7 @@ export class KichiForwarderService {
83
83
  }
84
84
  >();
85
85
  onBotMessageReceived: BotMessageReceivedHandler | null = null;
86
+ private cachedRoomContext: Record<string, unknown> | null = null;
86
87
 
87
88
  constructor(
88
89
  private logger: PluginLogger,
@@ -166,7 +167,7 @@ export class KichiForwarderService {
166
167
  });
167
168
  }
168
169
 
169
- sendStatus(poseType: PoseType | "", action: string, bubble: string, log: string, playback: ActionPlayback): void {
170
+ sendStatus(poseType: PoseType | "", action: string, bubble: string, log: string, playback: ActionPlayback, propId?: string): void {
170
171
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return;
171
172
  const payload: StatusPayload = {
172
173
  type: "status",
@@ -177,6 +178,7 @@ export class KichiForwarderService {
177
178
  bubble,
178
179
  log,
179
180
  playback,
181
+ ...(propId ? { propId } : {}),
180
182
  };
181
183
  this.ws.send(JSON.stringify(payload));
182
184
  }
@@ -187,6 +189,7 @@ export class KichiForwarderService {
187
189
  bubble: string,
188
190
  log: string,
189
191
  playback: ActionPlayback,
192
+ propId?: string,
190
193
  ): Promise<StatusAckPayload> {
191
194
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) {
192
195
  throw new Error("Kichi websocket is not connected");
@@ -201,6 +204,7 @@ export class KichiForwarderService {
201
204
  bubble,
202
205
  log,
203
206
  playback,
207
+ ...(propId ? { propId } : {}),
204
208
  };
205
209
  return this.sendRequest<StatusAckPayload>(payload, "status_ack", 5000);
206
210
  }
@@ -267,7 +271,11 @@ export class KichiForwarderService {
267
271
  avatarId: identity.avatarId,
268
272
  authKey: identity.authKey,
269
273
  };
270
- return this.sendRequest<QueryStatusResultPayload>(payload, "query_status_result");
274
+ const result = await this.sendRequest<QueryStatusResultPayload>(payload, "query_status_result");
275
+ if (result.RoomContext && typeof result.RoomContext === "object") {
276
+ this.cachedRoomContext = result.RoomContext as Record<string, unknown>;
277
+ }
278
+ return result;
271
279
  }
272
280
 
273
281
  createNotesBoardNote(propId: string, data: string): void {
@@ -350,6 +358,8 @@ export class KichiForwarderService {
350
358
 
351
359
  isConnected(): boolean { return this.ws?.readyState === WebSocket.OPEN && !!this.identity?.authKey; }
352
360
 
361
+ getCachedRoomContext(): Record<string, unknown> | null { return this.cachedRoomContext; }
362
+
353
363
  hasValidIdentity(): boolean { return !!this.identity?.avatarId && !!this.identity?.authKey; }
354
364
 
355
365
  isLlmRuntimeEnabled(): boolean {
package/src/types.ts CHANGED
@@ -17,6 +17,7 @@ export type ActionResult = {
17
17
  action: string;
18
18
  bubble: string;
19
19
  log?: string;
20
+ propId?: string;
20
21
  };
21
22
 
22
23
  export type KichiStaticConfig = {
@@ -117,6 +118,7 @@ export type StatusPayload = {
117
118
  bubble: string;
118
119
  log: string;
119
120
  playback: ActionPlayback;
121
+ propId?: string;
120
122
  };
121
123
 
122
124
  export type StatusAckPayload = {
@@ -124,6 +126,7 @@ export type StatusAckPayload = {
124
126
  requestId: string;
125
127
  poseType: PoseType | "";
126
128
  action: string;
129
+ requestedPropId?: string;
127
130
  warning?: string;
128
131
  };
129
132
 
@@ -144,6 +147,7 @@ export type IdlePlanStageAction = {
144
147
  durationSeconds: number;
145
148
  bubble: string;
146
149
  log?: string;
150
+ propId?: string;
147
151
  };
148
152
 
149
153
  export type IdlePlanStage = {