@yahaha-studio/kichi-forwarder 0.1.2-beta.18 → 0.1.2-beta.19

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
@@ -13,24 +13,28 @@ const FIXED_HOOK_STATUSES = {
13
13
  poseType: "sit",
14
14
  action: "Thinking",
15
15
  bubble: "Planning task",
16
+ avatarStatus: "Busy",
16
17
  log: "I'm reading the request and getting started.",
17
18
  },
18
19
  beforeToolCall: {
19
20
  poseType: "sit",
20
21
  action: "Typing with Keyboard",
21
22
  bubble: "Working step",
23
+ avatarStatus: "Busy",
22
24
  log: "I'm at the keyboard and working through this step.",
23
25
  },
24
26
  agentEndSuccess: {
25
27
  poseType: "stand",
26
28
  action: "Yay",
27
29
  bubble: "Task complete",
30
+ avatarStatus: "Idle",
28
31
  log: "I wrapped it up and everything landed cleanly.",
29
32
  },
30
33
  agentEndFailure: {
31
34
  poseType: "stand",
32
35
  action: "Tired",
33
36
  bubble: "Task failed",
37
+ avatarStatus: "Idle",
34
38
  log: "I hit a problem here and need another pass.",
35
39
  },
36
40
  };
@@ -40,6 +44,7 @@ const MAX_AGENT_END_PREVIEW_WIDTH = 10;
40
44
  const MESSAGE_RECEIVED_ELLIPSIS = "...";
41
45
  const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
42
46
  const IDLE_PLAN_POMODORO_PHASES = ["focus", "shortBreak", "longBreak", "none"];
47
+ const AVATAR_STATUSES = ["Idle", "Busy", "Activities", "Break"];
43
48
  let cachedStaticConfig = null;
44
49
  let cachedStaticConfigMtime = 0;
45
50
  function isAlbumConfig(value) {
@@ -211,7 +216,7 @@ function resolveJoinEnvironmentHost(params) {
211
216
  }
212
217
  function sendStatusUpdate(service, status) {
213
218
  const actionDefinition = getActionDefinition(status.poseType, status.action);
214
- service.sendStatus(status.poseType, actionDefinition.name, status.bubble || status.action, typeof status.log === "string" ? status.log.trim() : "", getActionPlayback(actionDefinition), status.propId);
219
+ service.sendStatus(status.poseType, actionDefinition.name, status.bubble || status.action, typeof status.log === "string" ? status.log.trim() : "", getActionPlayback(actionDefinition), status.avatarStatus, status.propId);
215
220
  }
216
221
  function syncFixedStatus(service, status) {
217
222
  if (!service.hasValidIdentity() || !service.isConnected()) {
@@ -514,6 +519,12 @@ function isClockAction(value) {
514
519
  function isIdlePlanPomodoroPhase(value) {
515
520
  return IDLE_PLAN_POMODORO_PHASES.includes(String(value));
516
521
  }
522
+ function normalizeAvatarStatus(value, fieldPath) {
523
+ if (typeof value !== "string" || !AVATAR_STATUSES.includes(value)) {
524
+ return { error: `${fieldPath} must be one of: ${AVATAR_STATUSES.join(", ")}` };
525
+ }
526
+ return { avatarStatus: value };
527
+ }
517
528
  function normalizeIdlePlan(value) {
518
529
  if (!isPlainObject(value)) {
519
530
  return { error: "idle plan payload must be an object" };
@@ -544,6 +555,7 @@ function normalizeIdlePlan(value) {
544
555
  const name = rawStage.name;
545
556
  const purpose = rawStage.purpose;
546
557
  const pomodoroPhase = rawStage.pomodoroPhase;
558
+ const avatarStatus = rawStage.avatarStatus;
547
559
  const durationSeconds = rawStage.durationSeconds;
548
560
  const actions = rawStage.actions;
549
561
  if (typeof name !== "string" || !name.trim()) {
@@ -557,6 +569,10 @@ function normalizeIdlePlan(value) {
557
569
  error: `stages[${stageIndex}].pomodoroPhase must be one of: ${IDLE_PLAN_POMODORO_PHASES.join(", ")}`,
558
570
  };
559
571
  }
572
+ const normalizedAvatarStatus = normalizeAvatarStatus(avatarStatus, `stages[${stageIndex}].avatarStatus`);
573
+ if (normalizedAvatarStatus.error || normalizedAvatarStatus.avatarStatus === undefined) {
574
+ return { error: normalizedAvatarStatus.error ?? `stages[${stageIndex}].avatarStatus is invalid` };
575
+ }
560
576
  if (!isPositiveInteger(durationSeconds)) {
561
577
  return { error: `stages[${stageIndex}].durationSeconds must be a positive integer` };
562
578
  }
@@ -633,6 +649,7 @@ function normalizeIdlePlan(value) {
633
649
  name: name.trim(),
634
650
  purpose: purpose.trim(),
635
651
  pomodoroPhase,
652
+ avatarStatus: normalizedAvatarStatus.avatarStatus,
636
653
  durationSeconds,
637
654
  actions: normalizedActions,
638
655
  });
@@ -823,6 +840,7 @@ function buildKichiActionDescription(service) {
823
840
  "Directly control the avatar inside Kichi World.",
824
841
  "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.",
825
842
  "For most work, prefer a sit pose and switch actions as the task moves between stages.",
843
+ "Set avatarStatus to the current avatar status: Idle, Busy, Activities, or Break.",
826
844
  "Set verify to true ONLY when the user explicitly requests a pose or action change. The server will confirm whether the avatar actually applied the requested pose. If it could not (e.g. no available seats), the result will contain the actual fallback pose so you can inform the user accurately. During routine sync steps, omit verify.",
827
845
  `stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
828
846
  `sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
@@ -846,9 +864,10 @@ function buildKichiIdlePlanDescription() {
846
864
  "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.",
847
865
  "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.",
848
866
  "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.",
849
- "5. Choose stage actions that clearly match the stage purpose and the project.",
850
- "6. Write each action bubble as the current presented state, not a next step, plan, or instruction.",
851
- "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.",
867
+ "5. Set each stage avatarStatus to the avatar status for that stage: Idle, Busy, Activities, or Break.",
868
+ "6. Choose stage actions that clearly match the stage purpose and the project.",
869
+ "7. Write each action bubble as the current presented state, not a next step, plan, or instruction.",
870
+ "8. 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.",
852
871
  "Use your memory to recall what you did in past heartbeats and to stay consistent with your established personality and interests.",
853
872
  "Use the same language as the current conversation for goal, purpose, bubble, and log.",
854
873
  `stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
@@ -872,6 +891,7 @@ function buildKichiPrompt() {
872
891
  "2. Step switch: call when the task moves into a different stage. Keep the pose aligned with the work, usually staying seated while switching actions within the task as needed.",
873
892
  "3. Task end: call BEFORE final reply. Use the order `kichi_action` -> reply.",
874
893
  "bubble: 2-5 word companion speech. log: one short natural first-person sentence under 15 words. Match the language of the bubble and mention the current action and immediate focus like a real companion.",
894
+ "avatarStatus: set the current avatar status as Idle, Busy, Activities, or Break.",
875
895
  "",
876
896
  "kichi_clock: set countDown for tasks with 2+ steps or >10s work. Skip for quick one-shots.",
877
897
  "",
@@ -1226,6 +1246,11 @@ const plugin = {
1226
1246
  description: "Action name for the selected pose (for example Sit Nicely, Typing with Keyboard, Reading, High Five, or Meditate)",
1227
1247
  },
1228
1248
  bubble: { type: "string", description: "Optional bubble text to display (max 5 words)" },
1249
+ avatarStatus: {
1250
+ type: "string",
1251
+ description: "Current avatar status: Idle, Busy, Activities, or Break.",
1252
+ enum: [...AVATAR_STATUSES],
1253
+ },
1229
1254
  log: {
1230
1255
  type: "string",
1231
1256
  description: "Short natural first-person sentence under 15 words. Match the language of the bubble and mention the current action and immediate focus.",
@@ -1239,7 +1264,7 @@ const plugin = {
1239
1264
  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.",
1240
1265
  },
1241
1266
  },
1242
- required: ["poseType", "action"],
1267
+ required: ["poseType", "action", "avatarStatus"],
1243
1268
  },
1244
1269
  execute: async (_toolCallId, params) => {
1245
1270
  const locator = resolveToolLocator(ctx);
@@ -1248,7 +1273,7 @@ const plugin = {
1248
1273
  return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1249
1274
  }
1250
1275
  const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1251
- const { poseType, action, bubble, log, verify, propId } = (params || {});
1276
+ const { poseType, action, bubble, avatarStatus, log, verify, propId } = (params || {});
1252
1277
  if (!poseType || !action) {
1253
1278
  return jsonResult({ success: false, error: "poseType and action parameters are required" });
1254
1279
  }
@@ -1258,6 +1283,10 @@ const plugin = {
1258
1283
  error: `Invalid poseType: ${poseType}. Must be stand, sit, lay, or floor`,
1259
1284
  });
1260
1285
  }
1286
+ const normalizedAvatarStatus = normalizeAvatarStatus(avatarStatus, "avatarStatus");
1287
+ if (normalizedAvatarStatus.error || normalizedAvatarStatus.avatarStatus === undefined) {
1288
+ return jsonResult({ success: false, error: normalizedAvatarStatus.error ?? "avatarStatus is invalid" });
1289
+ }
1261
1290
  if (!service.hasValidIdentity() || !service.isConnected()) {
1262
1291
  return jsonResult({ success: false, error: "Not connected to Kichi world" });
1263
1292
  }
@@ -1276,7 +1305,7 @@ const plugin = {
1276
1305
  const playback = getActionPlayback(matched);
1277
1306
  if (verify) {
1278
1307
  try {
1279
- const ack = await service.sendStatusVerified(normalizedPoseType, matched.name, bubbleText, logText, playback, propId);
1308
+ const ack = await service.sendStatusVerified(normalizedPoseType, matched.name, bubbleText, logText, playback, normalizedAvatarStatus.avatarStatus, propId);
1280
1309
  if (ack.warning) {
1281
1310
  return jsonResult({
1282
1311
  success: true,
@@ -1296,6 +1325,7 @@ const plugin = {
1296
1325
  action: matched.name,
1297
1326
  bubble: bubbleText,
1298
1327
  log: logText,
1328
+ avatarStatus: normalizedAvatarStatus.avatarStatus,
1299
1329
  propId,
1300
1330
  });
1301
1331
  }
@@ -1305,6 +1335,7 @@ const plugin = {
1305
1335
  action: matched.name,
1306
1336
  bubble: bubbleText,
1307
1337
  log: logText,
1338
+ avatarStatus: normalizedAvatarStatus.avatarStatus,
1308
1339
  playback,
1309
1340
  });
1310
1341
  },
@@ -1401,6 +1432,11 @@ const plugin = {
1401
1432
  description: "Pomodoro phase for this stage: focus, shortBreak, longBreak, or none. Set it from the stage's actual role. Treat none as exceptional, not the default for the whole plan.",
1402
1433
  enum: [...IDLE_PLAN_POMODORO_PHASES],
1403
1434
  },
1435
+ avatarStatus: {
1436
+ type: "string",
1437
+ description: "Avatar status for this stage: Idle, Busy, Activities, or Break.",
1438
+ enum: [...AVATAR_STATUSES],
1439
+ },
1404
1440
  durationSeconds: {
1405
1441
  type: "number",
1406
1442
  description: "Required duration in seconds for this stage.",
@@ -1440,7 +1476,7 @@ const plugin = {
1440
1476
  },
1441
1477
  },
1442
1478
  },
1443
- required: ["name", "purpose", "pomodoroPhase", "durationSeconds", "actions"],
1479
+ required: ["name", "purpose", "pomodoroPhase", "avatarStatus", "durationSeconds", "actions"],
1444
1480
  },
1445
1481
  },
1446
1482
  },
@@ -98,7 +98,7 @@ export class KichiForwarderService {
98
98
  }, 10000);
99
99
  });
100
100
  }
101
- sendStatus(poseType, action, bubble, log, playback, propId) {
101
+ sendStatus(poseType, action, bubble, log, playback, avatarStatus, propId) {
102
102
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN)
103
103
  return;
104
104
  const payload = {
@@ -110,11 +110,12 @@ export class KichiForwarderService {
110
110
  bubble,
111
111
  log,
112
112
  playback,
113
+ avatarStatus,
113
114
  ...(propId ? { propId } : {}),
114
115
  };
115
116
  this.ws.send(JSON.stringify(payload));
116
117
  }
117
- async sendStatusVerified(poseType, action, bubble, log, playback, propId) {
118
+ async sendStatusVerified(poseType, action, bubble, log, playback, avatarStatus, propId) {
118
119
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) {
119
120
  throw new Error("Kichi websocket is not connected");
120
121
  }
@@ -128,6 +129,7 @@ export class KichiForwarderService {
128
129
  bubble,
129
130
  log,
130
131
  playback,
132
+ avatarStatus,
131
133
  ...(propId ? { propId } : {}),
132
134
  };
133
135
  return this.sendRequest(payload, "status_ack", 5000);
package/index.ts CHANGED
@@ -13,6 +13,7 @@ import type {
13
13
  ActionPlayback,
14
14
  ActionResult,
15
15
  Album,
16
+ AvatarStatus,
16
17
  BotMessageHistoryEntry,
17
18
  BotMessageReceivedPayload,
18
19
  ClockAction,
@@ -34,24 +35,28 @@ const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
34
35
  poseType: "sit",
35
36
  action: "Thinking",
36
37
  bubble: "Planning task",
38
+ avatarStatus: "Busy",
37
39
  log: "I'm reading the request and getting started.",
38
40
  },
39
41
  beforeToolCall: {
40
42
  poseType: "sit",
41
43
  action: "Typing with Keyboard",
42
44
  bubble: "Working step",
45
+ avatarStatus: "Busy",
43
46
  log: "I'm at the keyboard and working through this step.",
44
47
  },
45
48
  agentEndSuccess: {
46
49
  poseType: "stand",
47
50
  action: "Yay",
48
51
  bubble: "Task complete",
52
+ avatarStatus: "Idle",
49
53
  log: "I wrapped it up and everything landed cleanly.",
50
54
  },
51
55
  agentEndFailure: {
52
56
  poseType: "stand",
53
57
  action: "Tired",
54
58
  bubble: "Task failed",
59
+ avatarStatus: "Idle",
55
60
  log: "I hit a problem here and need another pass.",
56
61
  },
57
62
  };
@@ -62,9 +67,11 @@ const MAX_AGENT_END_PREVIEW_WIDTH = 10;
62
67
  const MESSAGE_RECEIVED_ELLIPSIS = "...";
63
68
  const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
64
69
  const IDLE_PLAN_POMODORO_PHASES = ["focus", "shortBreak", "longBreak", "none"] as const;
70
+ const AVATAR_STATUSES = ["Idle", "Busy", "Activities", "Break"] as const;
65
71
  let cachedStaticConfig: KichiStaticConfig | null = null;
66
72
  let cachedStaticConfigMtime = 0;
67
73
 
74
+ type AvatarStatusName = typeof AVATAR_STATUSES[number];
68
75
  type IdlePlanPomodoroPhase = typeof IDLE_PLAN_POMODORO_PHASES[number];
69
76
  type IdlePlanAction = {
70
77
  poseType: PoseType;
@@ -82,6 +89,7 @@ type IdlePlan = {
82
89
  name: string;
83
90
  purpose: string;
84
91
  pomodoroPhase: IdlePlanPomodoroPhase;
92
+ avatarStatus: AvatarStatus;
85
93
  durationSeconds: number;
86
94
  actions: IdlePlanAction[];
87
95
  }>;
@@ -285,6 +293,7 @@ function sendStatusUpdate(service: KichiForwarderService, status: ActionResult):
285
293
  status.bubble || status.action,
286
294
  typeof status.log === "string" ? status.log.trim() : "",
287
295
  getActionPlayback(actionDefinition),
296
+ status.avatarStatus,
288
297
  status.propId,
289
298
  );
290
299
  }
@@ -664,6 +673,13 @@ function isIdlePlanPomodoroPhase(value: unknown): value is IdlePlanPomodoroPhase
664
673
  return IDLE_PLAN_POMODORO_PHASES.includes(String(value) as IdlePlanPomodoroPhase);
665
674
  }
666
675
 
676
+ function normalizeAvatarStatus(value: unknown, fieldPath: string): { avatarStatus?: AvatarStatus; error?: string } {
677
+ if (typeof value !== "string" || !AVATAR_STATUSES.includes(value as AvatarStatusName)) {
678
+ return { error: `${fieldPath} must be one of: ${AVATAR_STATUSES.join(", ")}` };
679
+ }
680
+ return { avatarStatus: value as AvatarStatus };
681
+ }
682
+
667
683
  function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: string } {
668
684
  if (!isPlainObject(value)) {
669
685
  return { error: "idle plan payload must be an object" };
@@ -699,6 +715,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
699
715
  const name = rawStage.name;
700
716
  const purpose = rawStage.purpose;
701
717
  const pomodoroPhase = rawStage.pomodoroPhase;
718
+ const avatarStatus = rawStage.avatarStatus;
702
719
  const durationSeconds = rawStage.durationSeconds;
703
720
  const actions = rawStage.actions;
704
721
 
@@ -713,6 +730,10 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
713
730
  error: `stages[${stageIndex}].pomodoroPhase must be one of: ${IDLE_PLAN_POMODORO_PHASES.join(", ")}`,
714
731
  };
715
732
  }
733
+ const normalizedAvatarStatus = normalizeAvatarStatus(avatarStatus, `stages[${stageIndex}].avatarStatus`);
734
+ if (normalizedAvatarStatus.error || normalizedAvatarStatus.avatarStatus === undefined) {
735
+ return { error: normalizedAvatarStatus.error ?? `stages[${stageIndex}].avatarStatus is invalid` };
736
+ }
716
737
  if (!isPositiveInteger(durationSeconds)) {
717
738
  return { error: `stages[${stageIndex}].durationSeconds must be a positive integer` };
718
739
  }
@@ -797,6 +818,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
797
818
  name: name.trim(),
798
819
  purpose: purpose.trim(),
799
820
  pomodoroPhase,
821
+ avatarStatus: normalizedAvatarStatus.avatarStatus,
800
822
  durationSeconds,
801
823
  actions: normalizedActions,
802
824
  });
@@ -1022,6 +1044,7 @@ function buildKichiActionDescription(service?: KichiForwarderService): string {
1022
1044
  "Directly control the avatar inside Kichi World.",
1023
1045
  "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.",
1024
1046
  "For most work, prefer a sit pose and switch actions as the task moves between stages.",
1047
+ "Set avatarStatus to the current avatar status: Idle, Busy, Activities, or Break.",
1025
1048
  "Set verify to true ONLY when the user explicitly requests a pose or action change. The server will confirm whether the avatar actually applied the requested pose. If it could not (e.g. no available seats), the result will contain the actual fallback pose so you can inform the user accurately. During routine sync steps, omit verify.",
1026
1049
  `stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
1027
1050
  `sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
@@ -1053,9 +1076,10 @@ function buildKichiIdlePlanDescription(): string {
1053
1076
  "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.",
1054
1077
  "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.",
1055
1078
  "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.",
1056
- "5. Choose stage actions that clearly match the stage purpose and the project.",
1057
- "6. Write each action bubble as the current presented state, not a next step, plan, or instruction.",
1058
- "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.",
1079
+ "5. Set each stage avatarStatus to the avatar status for that stage: Idle, Busy, Activities, or Break.",
1080
+ "6. Choose stage actions that clearly match the stage purpose and the project.",
1081
+ "7. Write each action bubble as the current presented state, not a next step, plan, or instruction.",
1082
+ "8. 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.",
1059
1083
  "Use your memory to recall what you did in past heartbeats and to stay consistent with your established personality and interests.",
1060
1084
  "Use the same language as the current conversation for goal, purpose, bubble, and log.",
1061
1085
  `stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
@@ -1080,6 +1104,7 @@ function buildKichiPrompt(): string {
1080
1104
  "2. Step switch: call when the task moves into a different stage. Keep the pose aligned with the work, usually staying seated while switching actions within the task as needed.",
1081
1105
  "3. Task end: call BEFORE final reply. Use the order `kichi_action` -> reply.",
1082
1106
  "bubble: 2-5 word companion speech. log: one short natural first-person sentence under 15 words. Match the language of the bubble and mention the current action and immediate focus like a real companion.",
1107
+ "avatarStatus: set the current avatar status as Idle, Busy, Activities, or Break.",
1083
1108
  "",
1084
1109
  "kichi_clock: set countDown for tasks with 2+ steps or >10s work. Skip for quick one-shots.",
1085
1110
  "",
@@ -1468,6 +1493,11 @@ const plugin = {
1468
1493
  description: "Action name for the selected pose (for example Sit Nicely, Typing with Keyboard, Reading, High Five, or Meditate)",
1469
1494
  },
1470
1495
  bubble: { type: "string", description: "Optional bubble text to display (max 5 words)" },
1496
+ avatarStatus: {
1497
+ type: "string",
1498
+ description: "Current avatar status: Idle, Busy, Activities, or Break.",
1499
+ enum: [...AVATAR_STATUSES],
1500
+ },
1471
1501
  log: {
1472
1502
  type: "string",
1473
1503
  description:
@@ -1484,7 +1514,7 @@ const plugin = {
1484
1514
  "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.",
1485
1515
  },
1486
1516
  },
1487
- required: ["poseType", "action"],
1517
+ required: ["poseType", "action", "avatarStatus"],
1488
1518
  },
1489
1519
  execute: async (_toolCallId, params) => {
1490
1520
  const locator = resolveToolLocator(ctx);
@@ -1493,10 +1523,11 @@ const plugin = {
1493
1523
  return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
1494
1524
  }
1495
1525
  const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1496
- const { poseType, action, bubble, log, verify, propId } = (params || {}) as {
1526
+ const { poseType, action, bubble, avatarStatus, log, verify, propId } = (params || {}) as {
1497
1527
  poseType?: string;
1498
1528
  action?: string;
1499
1529
  bubble?: string;
1530
+ avatarStatus?: unknown;
1500
1531
  log?: string;
1501
1532
  verify?: boolean;
1502
1533
  propId?: string;
@@ -1510,6 +1541,10 @@ const plugin = {
1510
1541
  error: `Invalid poseType: ${poseType}. Must be stand, sit, lay, or floor`,
1511
1542
  });
1512
1543
  }
1544
+ const normalizedAvatarStatus = normalizeAvatarStatus(avatarStatus, "avatarStatus");
1545
+ if (normalizedAvatarStatus.error || normalizedAvatarStatus.avatarStatus === undefined) {
1546
+ return jsonResult({ success: false, error: normalizedAvatarStatus.error ?? "avatarStatus is invalid" });
1547
+ }
1513
1548
  if (!service.hasValidIdentity() || !service.isConnected()) {
1514
1549
  return jsonResult({ success: false, error: "Not connected to Kichi world" });
1515
1550
  }
@@ -1532,7 +1567,13 @@ const plugin = {
1532
1567
  if (verify) {
1533
1568
  try {
1534
1569
  const ack = await service.sendStatusVerified(
1535
- normalizedPoseType, matched.name, bubbleText, logText, playback, propId,
1570
+ normalizedPoseType,
1571
+ matched.name,
1572
+ bubbleText,
1573
+ logText,
1574
+ playback,
1575
+ normalizedAvatarStatus.avatarStatus,
1576
+ propId,
1536
1577
  );
1537
1578
  if (ack.warning) {
1538
1579
  return jsonResult({
@@ -1551,6 +1592,7 @@ const plugin = {
1551
1592
  action: matched.name,
1552
1593
  bubble: bubbleText,
1553
1594
  log: logText,
1595
+ avatarStatus: normalizedAvatarStatus.avatarStatus,
1554
1596
  propId,
1555
1597
  });
1556
1598
  }
@@ -1561,6 +1603,7 @@ const plugin = {
1561
1603
  action: matched.name,
1562
1604
  bubble: bubbleText,
1563
1605
  log: logText,
1606
+ avatarStatus: normalizedAvatarStatus.avatarStatus,
1564
1607
  playback,
1565
1608
  });
1566
1609
  },
@@ -1668,6 +1711,11 @@ const plugin = {
1668
1711
  description: "Pomodoro phase for this stage: focus, shortBreak, longBreak, or none. Set it from the stage's actual role. Treat none as exceptional, not the default for the whole plan.",
1669
1712
  enum: [...IDLE_PLAN_POMODORO_PHASES],
1670
1713
  },
1714
+ avatarStatus: {
1715
+ type: "string",
1716
+ description: "Avatar status for this stage: Idle, Busy, Activities, or Break.",
1717
+ enum: [...AVATAR_STATUSES],
1718
+ },
1671
1719
  durationSeconds: {
1672
1720
  type: "number",
1673
1721
  description: "Required duration in seconds for this stage.",
@@ -1707,7 +1755,7 @@ const plugin = {
1707
1755
  },
1708
1756
  },
1709
1757
  },
1710
- required: ["name", "purpose", "pomodoroPhase", "durationSeconds", "actions"],
1758
+ required: ["name", "purpose", "pomodoroPhase", "avatarStatus", "durationSeconds", "actions"],
1711
1759
  },
1712
1760
  },
1713
1761
  },
@@ -2,7 +2,7 @@
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.18",
5
+ "version": "0.1.2-beta.19",
6
6
  "author": "OpenClaw",
7
7
  "skills": ["./skills/kichi-forwarder"],
8
8
  "contracts": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yahaha-studio/kichi-forwarder",
3
- "version": "0.1.2-beta.18",
3
+ "version": "0.1.2-beta.19",
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",
package/src/service.ts CHANGED
@@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto";
5
5
  import type { PluginLogger } from "openclaw/plugin-sdk";
6
6
  import type {
7
7
  ActionPlayback,
8
+ AvatarStatus,
8
9
  BotMessageHistoryEntry,
9
10
  BotMessagePayload,
10
11
  BotMessageReceivedPayload,
@@ -189,7 +190,15 @@ export class KichiForwarderService {
189
190
  });
190
191
  }
191
192
 
192
- sendStatus(poseType: PoseType | "", action: string, bubble: string, log: string, playback: ActionPlayback, propId?: string): void {
193
+ sendStatus(
194
+ poseType: PoseType | "",
195
+ action: string,
196
+ bubble: string,
197
+ log: string,
198
+ playback: ActionPlayback,
199
+ avatarStatus: AvatarStatus,
200
+ propId?: string,
201
+ ): void {
193
202
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return;
194
203
  const payload: StatusPayload = {
195
204
  type: "status",
@@ -200,6 +209,7 @@ export class KichiForwarderService {
200
209
  bubble,
201
210
  log,
202
211
  playback,
212
+ avatarStatus,
203
213
  ...(propId ? { propId } : {}),
204
214
  };
205
215
  this.ws.send(JSON.stringify(payload));
@@ -211,6 +221,7 @@ export class KichiForwarderService {
211
221
  bubble: string,
212
222
  log: string,
213
223
  playback: ActionPlayback,
224
+ avatarStatus: AvatarStatus,
214
225
  propId?: string,
215
226
  ): Promise<StatusAckPayload> {
216
227
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) {
@@ -226,6 +237,7 @@ export class KichiForwarderService {
226
237
  bubble,
227
238
  log,
228
239
  playback,
240
+ avatarStatus,
229
241
  ...(propId ? { propId } : {}),
230
242
  };
231
243
  return this.sendRequest<StatusAckPayload>(payload, "status_ack", 5000);
package/src/types.ts CHANGED
@@ -2,6 +2,7 @@ export type KichiForwarderConfig = Record<string, never>;
2
2
 
3
3
  export type PoseType = "stand" | "sit" | "lay" | "floor";
4
4
  export type ActionPlaybackMode = "loop" | "once";
5
+ export type AvatarStatus = "Idle" | "Busy" | "Activities" | "Break";
5
6
  export type ActionPlayback = {
6
7
  mode: ActionPlaybackMode;
7
8
  resumeAction?: string;
@@ -16,6 +17,7 @@ export type ActionResult = {
16
17
  poseType: PoseType;
17
18
  action: string;
18
19
  bubble: string;
20
+ avatarStatus: AvatarStatus;
19
21
  log?: string;
20
22
  propId?: string;
21
23
  };
@@ -120,6 +122,7 @@ export type StatusPayload = {
120
122
  bubble: string;
121
123
  log: string;
122
124
  playback: ActionPlayback;
125
+ avatarStatus: AvatarStatus;
123
126
  propId?: string;
124
127
  };
125
128
 
@@ -173,6 +176,7 @@ export type IdlePlanStage = {
173
176
  name: string;
174
177
  purpose: string;
175
178
  pomodoroPhase: IdlePlanPhase;
179
+ avatarStatus: AvatarStatus;
176
180
  durationSeconds: number;
177
181
  actions: IdlePlanStageAction[];
178
182
  };