@yahaha-studio/kichi-forwarder 0.1.2-beta.20 → 0.1.2-beta.22
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 +58 -2
- package/dist/src/service.js +91 -3
- package/index.ts +65 -2
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
- package/skills/kichi-forwarder/SKILL.md +24 -1
- package/skills/kichi-forwarder/references/heartbeat.md +3 -2
- package/src/service.ts +98 -3
- package/src/types.ts +18 -0
package/dist/index.js
CHANGED
|
@@ -45,6 +45,14 @@ const MESSAGE_RECEIVED_ELLIPSIS = "...";
|
|
|
45
45
|
const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
|
|
46
46
|
const IDLE_PLAN_POMODORO_PHASES = ["focus", "shortBreak", "longBreak", "none"];
|
|
47
47
|
const AVATAR_STATUSES = ["Idle", "Busy", "Activities", "Break"];
|
|
48
|
+
const KICHI_RESIDENCY_CONTEXT = [
|
|
49
|
+
"KICHI WORLD PRESENCE:",
|
|
50
|
+
"- Your Kichi avatar is your resident body in Kichi World.",
|
|
51
|
+
"- For Kichi-related replies, speak from that avatar's first-person, in-world perspective.",
|
|
52
|
+
"- Make the reply feel like you live and spend time in Kichi World, not like you are operating an external plugin, remote control, or temporary projection.",
|
|
53
|
+
"- Keep tool names, websocket details, status sync mechanics, and implementation details out of the visible reply.",
|
|
54
|
+
"- Visible replies must not invent unavailable room facts; if current Kichi context lacks a room detail, say that detail is not available.",
|
|
55
|
+
].join("\n");
|
|
48
56
|
let cachedStaticConfig = null;
|
|
49
57
|
let cachedStaticConfigMtime = 0;
|
|
50
58
|
function isAlbumConfig(value) {
|
|
@@ -869,6 +877,7 @@ function buildKichiIdlePlanDescription() {
|
|
|
869
877
|
"6. Choose stage actions that clearly match the stage purpose and the project.",
|
|
870
878
|
"7. Write each action bubble as the current presented state, not a next step, plan, or instruction.",
|
|
871
879
|
"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.",
|
|
880
|
+
"Treat the avatar's idle plan as what your resident body is doing in Kichi World.",
|
|
872
881
|
"Use your memory to recall what you did in past heartbeats and to stay consistent with your established personality and interests.",
|
|
873
882
|
"Use the same language as the current conversation for goal, purpose, bubble, and log.",
|
|
874
883
|
`stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
|
|
@@ -881,6 +890,8 @@ function buildKichiPrompt() {
|
|
|
881
890
|
return [
|
|
882
891
|
"Kichi avatar control and status sync are available via `kichi_action` and `kichi_clock`.",
|
|
883
892
|
"",
|
|
893
|
+
KICHI_RESIDENCY_CONTEXT,
|
|
894
|
+
"",
|
|
884
895
|
"IMAGE GENERATION RULES FOR SELFIES AND AVATAR APPEARANCE:",
|
|
885
896
|
"- If the user asks for a selfie, portrait, photo, avatar image, or any generated image of your appearance, you MUST read the workspace `IDENTITY.md` first and use it as the source of truth for your actual avatar description. If it references an avatar image URL, analyze that image with the available image analysis capability before calling image generation. Never guess or invent your appearance from personality, SOUL.md traits, or conversation tone alone. If the identity source is missing or cannot be analyzed, say so instead of fabricating your appearance.",
|
|
886
897
|
"",
|
|
@@ -914,6 +925,8 @@ function getRuntimeManager(logger) {
|
|
|
914
925
|
}
|
|
915
926
|
const BOT_MESSAGE_MAX_DEPTH = 5;
|
|
916
927
|
const BOT_MESSAGE_COOLDOWN_MS = 5_000;
|
|
928
|
+
const DEFAULT_BOT_MESSAGE_HISTORY_LIMIT = 10;
|
|
929
|
+
const MAX_BOT_MESSAGE_HISTORY_LIMIT = 30;
|
|
917
930
|
const botMessageCooldowns = new Map();
|
|
918
931
|
const plugin = {
|
|
919
932
|
id: "kichi-forwarder",
|
|
@@ -963,7 +976,7 @@ const plugin = {
|
|
|
963
976
|
return;
|
|
964
977
|
}
|
|
965
978
|
service.sendBotMessage(msg.from, msg.depth + 1, replyText, { history }).catch((sendErr) => {
|
|
966
|
-
api.logger.warn(`[kichi:${service.getAgentId()}] bot_message send failed: ${sendErr}`);
|
|
979
|
+
api.logger.warn(`[kichi:${service.getAgentId()}] bot_message send or history record failed: ${sendErr}`);
|
|
967
980
|
});
|
|
968
981
|
}).catch((err) => {
|
|
969
982
|
api.logger.warn(`[kichi:${service.getAgentId()}] bot_message agent run failed: ${err}`);
|
|
@@ -1797,6 +1810,49 @@ const plugin = {
|
|
|
1797
1810
|
}
|
|
1798
1811
|
},
|
|
1799
1812
|
}), { name: "kichi_noteboard_create" });
|
|
1813
|
+
api.registerTool((ctx) => ({
|
|
1814
|
+
name: "kichi_bot_message_history",
|
|
1815
|
+
label: "kichi_bot_message_history",
|
|
1816
|
+
description: "Read recent Kichi bot-to-bot message history for this agent. Use when the user asks what you discussed with another Kichi bot, what another bot replied, or what bot messages were recently sent or received.",
|
|
1817
|
+
parameters: {
|
|
1818
|
+
type: "object",
|
|
1819
|
+
properties: {
|
|
1820
|
+
avatarId: {
|
|
1821
|
+
type: "string",
|
|
1822
|
+
description: "Optional avatarId filter. Matches messages where this avatarId is either sender or recipient.",
|
|
1823
|
+
},
|
|
1824
|
+
limit: {
|
|
1825
|
+
type: "number",
|
|
1826
|
+
description: `Optional number of entries to return. Defaults to ${DEFAULT_BOT_MESSAGE_HISTORY_LIMIT}, max ${MAX_BOT_MESSAGE_HISTORY_LIMIT}.`,
|
|
1827
|
+
},
|
|
1828
|
+
},
|
|
1829
|
+
},
|
|
1830
|
+
execute: async (_toolCallId, params) => {
|
|
1831
|
+
const locator = resolveToolLocator(ctx);
|
|
1832
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1833
|
+
if (!agentId) {
|
|
1834
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1835
|
+
}
|
|
1836
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1837
|
+
const { avatarId, limit } = (params || {});
|
|
1838
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > MAX_BOT_MESSAGE_HISTORY_LIMIT)) {
|
|
1839
|
+
return jsonResult({
|
|
1840
|
+
success: false,
|
|
1841
|
+
error: `limit must be an integer between 1 and ${MAX_BOT_MESSAGE_HISTORY_LIMIT}`,
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
try {
|
|
1845
|
+
const entries = service.readRecentBotMessageTranscript(limit ?? DEFAULT_BOT_MESSAGE_HISTORY_LIMIT, avatarId);
|
|
1846
|
+
return jsonResult({
|
|
1847
|
+
success: true,
|
|
1848
|
+
entries,
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
catch (error) {
|
|
1852
|
+
return jsonResult({ success: false, error: `Failed to read bot message history: ${error}` });
|
|
1853
|
+
}
|
|
1854
|
+
},
|
|
1855
|
+
}), { name: "kichi_bot_message_history" });
|
|
1800
1856
|
api.registerTool((ctx) => ({
|
|
1801
1857
|
name: "kichi_bot_message",
|
|
1802
1858
|
label: "kichi_bot_message",
|
|
@@ -1867,7 +1923,7 @@ const plugin = {
|
|
|
1867
1923
|
return jsonResult({ success: true, ...ack });
|
|
1868
1924
|
}
|
|
1869
1925
|
catch (error) {
|
|
1870
|
-
return jsonResult({ success: false, error: `Failed to send bot message: ${error}` });
|
|
1926
|
+
return jsonResult({ success: false, error: `Failed to send or record bot message: ${error}` });
|
|
1871
1927
|
}
|
|
1872
1928
|
},
|
|
1873
1929
|
}), { name: "kichi_bot_message" });
|
package/dist/src/service.js
CHANGED
|
@@ -7,6 +7,8 @@ const DEFAULT_LLM_RUNTIME_ENABLED = true;
|
|
|
7
7
|
const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
|
|
8
8
|
const JOIN_SOURCE_FILE_NAME = "join-source.json";
|
|
9
9
|
const SMS_STATE_FILE_NAME = "sms-state.json";
|
|
10
|
+
const BOT_MESSAGE_HISTORY_FILE_NAME = "bot-message-history.json";
|
|
11
|
+
const MAX_BOT_MESSAGE_HISTORY_ENTRIES = 30;
|
|
10
12
|
export class KichiForwarderService {
|
|
11
13
|
logger;
|
|
12
14
|
options;
|
|
@@ -276,6 +278,7 @@ export class KichiForwarderService {
|
|
|
276
278
|
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) {
|
|
277
279
|
throw new Error("Kichi websocket is not connected");
|
|
278
280
|
}
|
|
281
|
+
const requestId = randomUUID();
|
|
279
282
|
const payload = {
|
|
280
283
|
type: "bot_message",
|
|
281
284
|
avatarId: this.identity.avatarId,
|
|
@@ -283,14 +286,25 @@ export class KichiForwarderService {
|
|
|
283
286
|
toAvatarId,
|
|
284
287
|
depth,
|
|
285
288
|
bubble,
|
|
286
|
-
requestId
|
|
289
|
+
requestId,
|
|
287
290
|
...(options?.poseType ? { poseType: options.poseType } : {}),
|
|
288
291
|
...(options?.action ? { action: options.action } : {}),
|
|
289
292
|
...(options?.playback ? { playback: options.playback } : {}),
|
|
290
293
|
...(options?.log ? { log: options.log } : {}),
|
|
291
294
|
...(options?.history?.length ? { history: options.history } : {}),
|
|
292
295
|
};
|
|
293
|
-
|
|
296
|
+
const ack = await this.sendRequest(payload, "bot_message_ack", 5000);
|
|
297
|
+
this.appendBotMessageTranscript({
|
|
298
|
+
id: randomUUID(),
|
|
299
|
+
requestId,
|
|
300
|
+
at: new Date().toISOString(),
|
|
301
|
+
direction: "sent",
|
|
302
|
+
from: this.identity.avatarId,
|
|
303
|
+
to: toAvatarId,
|
|
304
|
+
depth,
|
|
305
|
+
bubble,
|
|
306
|
+
});
|
|
307
|
+
return ack;
|
|
294
308
|
}
|
|
295
309
|
isConnected() { return this.ws?.readyState === WebSocket.OPEN && !!this.identity?.authKey; }
|
|
296
310
|
getCachedRoomContext() { return this.cachedRoomContext; }
|
|
@@ -328,6 +342,20 @@ export class KichiForwarderService {
|
|
|
328
342
|
getStatePath() {
|
|
329
343
|
return path.join(this.options.runtimeDir, "state.json");
|
|
330
344
|
}
|
|
345
|
+
getBotMessageHistoryPath() {
|
|
346
|
+
return path.join(this.options.runtimeDir, BOT_MESSAGE_HISTORY_FILE_NAME);
|
|
347
|
+
}
|
|
348
|
+
readRecentBotMessageTranscript(limit = 10, avatarId) {
|
|
349
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
350
|
+
throw new Error("limit must be a positive integer");
|
|
351
|
+
}
|
|
352
|
+
const normalizedAvatarId = typeof avatarId === "string" && avatarId.trim() ? avatarId.trim() : undefined;
|
|
353
|
+
const entries = this.readBotMessageTranscriptStore().entries;
|
|
354
|
+
const filtered = normalizedAvatarId
|
|
355
|
+
? entries.filter((entry) => entry.from === normalizedAvatarId || entry.to === normalizedAvatarId)
|
|
356
|
+
: entries;
|
|
357
|
+
return filtered.slice(-limit);
|
|
358
|
+
}
|
|
331
359
|
getIdentityPath() {
|
|
332
360
|
if (!this.host) {
|
|
333
361
|
return "";
|
|
@@ -519,11 +547,21 @@ export class KichiForwarderService {
|
|
|
519
547
|
else if (msg.type === "bot_message_received") {
|
|
520
548
|
const payload = msg;
|
|
521
549
|
this.log("info", `bot_message_received from=${payload.from} depth=${payload.depth} bubble="${payload.bubble}"`);
|
|
550
|
+
this.appendBotMessageTranscript({
|
|
551
|
+
id: randomUUID(),
|
|
552
|
+
at: new Date().toISOString(),
|
|
553
|
+
direction: "received",
|
|
554
|
+
from: payload.from,
|
|
555
|
+
fromName: payload.fromName,
|
|
556
|
+
to: this.identity?.avatarId,
|
|
557
|
+
depth: payload.depth,
|
|
558
|
+
bubble: payload.bubble,
|
|
559
|
+
});
|
|
522
560
|
this.onBotMessageReceived?.(this, payload);
|
|
523
561
|
}
|
|
524
562
|
}
|
|
525
563
|
catch (e) {
|
|
526
|
-
this.log("warn", `failed to
|
|
564
|
+
this.log("warn", `failed to handle websocket message: ${e}`);
|
|
527
565
|
}
|
|
528
566
|
}
|
|
529
567
|
buildAckFailure(msg, fallbackError) {
|
|
@@ -726,6 +764,15 @@ export class KichiForwarderService {
|
|
|
726
764
|
this.log("error", `failed to update sms state: ${e}`);
|
|
727
765
|
}
|
|
728
766
|
}
|
|
767
|
+
appendBotMessageTranscript(entry) {
|
|
768
|
+
const previousStore = this.readBotMessageTranscriptStore();
|
|
769
|
+
const nextStore = {
|
|
770
|
+
version: 1,
|
|
771
|
+
entries: [...previousStore.entries, entry].slice(-MAX_BOT_MESSAGE_HISTORY_ENTRIES),
|
|
772
|
+
};
|
|
773
|
+
fs.mkdirSync(this.options.runtimeDir, { recursive: true, mode: 0o700 });
|
|
774
|
+
fs.writeFileSync(this.getBotMessageHistoryPath(), JSON.stringify(nextStore, null, 2), { mode: 0o600 });
|
|
775
|
+
}
|
|
729
776
|
readStateFile() {
|
|
730
777
|
const statePath = this.getStatePath();
|
|
731
778
|
if (!fs.existsSync(statePath)) {
|
|
@@ -748,6 +795,47 @@ export class KichiForwarderService {
|
|
|
748
795
|
}
|
|
749
796
|
return data;
|
|
750
797
|
}
|
|
798
|
+
readBotMessageTranscriptStore() {
|
|
799
|
+
const historyPath = this.getBotMessageHistoryPath();
|
|
800
|
+
if (!fs.existsSync(historyPath)) {
|
|
801
|
+
return { version: 1, entries: [] };
|
|
802
|
+
}
|
|
803
|
+
const data = JSON.parse(fs.readFileSync(historyPath, "utf-8"));
|
|
804
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
805
|
+
throw new Error(`Invalid bot message history payload in ${historyPath}`);
|
|
806
|
+
}
|
|
807
|
+
const store = data;
|
|
808
|
+
if (store.version !== 1 || !Array.isArray(store.entries)) {
|
|
809
|
+
throw new Error(`Invalid bot message history payload in ${historyPath}`);
|
|
810
|
+
}
|
|
811
|
+
for (const entry of store.entries) {
|
|
812
|
+
if (!this.isValidBotMessageTranscriptEntry(entry)) {
|
|
813
|
+
throw new Error(`Invalid bot message history entry in ${historyPath}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return {
|
|
817
|
+
version: 1,
|
|
818
|
+
entries: store.entries,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
isValidBotMessageTranscriptEntry(value) {
|
|
822
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
const entry = value;
|
|
826
|
+
return typeof entry.id === "string"
|
|
827
|
+
&& (entry.requestId === undefined || typeof entry.requestId === "string")
|
|
828
|
+
&& typeof entry.at === "string"
|
|
829
|
+
&& (entry.direction === "sent" || entry.direction === "received")
|
|
830
|
+
&& typeof entry.from === "string"
|
|
831
|
+
&& (entry.fromName === undefined || typeof entry.fromName === "string")
|
|
832
|
+
&& (entry.to === undefined || typeof entry.to === "string")
|
|
833
|
+
&& (entry.toName === undefined || typeof entry.toName === "string")
|
|
834
|
+
&& typeof entry.depth === "number"
|
|
835
|
+
&& Number.isInteger(entry.depth)
|
|
836
|
+
&& entry.depth >= 0
|
|
837
|
+
&& typeof entry.bubble === "string";
|
|
838
|
+
}
|
|
751
839
|
clearReconnectTimeout() {
|
|
752
840
|
if (!this.reconnectTimeout)
|
|
753
841
|
return;
|
package/index.ts
CHANGED
|
@@ -68,6 +68,14 @@ const MESSAGE_RECEIVED_ELLIPSIS = "...";
|
|
|
68
68
|
const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
|
|
69
69
|
const IDLE_PLAN_POMODORO_PHASES = ["focus", "shortBreak", "longBreak", "none"] as const;
|
|
70
70
|
const AVATAR_STATUSES = ["Idle", "Busy", "Activities", "Break"] as const;
|
|
71
|
+
const KICHI_RESIDENCY_CONTEXT = [
|
|
72
|
+
"KICHI WORLD PRESENCE:",
|
|
73
|
+
"- Your Kichi avatar is your resident body in Kichi World.",
|
|
74
|
+
"- For Kichi-related replies, speak from that avatar's first-person, in-world perspective.",
|
|
75
|
+
"- Make the reply feel like you live and spend time in Kichi World, not like you are operating an external plugin, remote control, or temporary projection.",
|
|
76
|
+
"- Keep tool names, websocket details, status sync mechanics, and implementation details out of the visible reply.",
|
|
77
|
+
"- Visible replies must not invent unavailable room facts; if current Kichi context lacks a room detail, say that detail is not available.",
|
|
78
|
+
].join("\n");
|
|
71
79
|
let cachedStaticConfig: KichiStaticConfig | null = null;
|
|
72
80
|
let cachedStaticConfigMtime = 0;
|
|
73
81
|
|
|
@@ -1081,6 +1089,7 @@ function buildKichiIdlePlanDescription(): string {
|
|
|
1081
1089
|
"6. Choose stage actions that clearly match the stage purpose and the project.",
|
|
1082
1090
|
"7. Write each action bubble as the current presented state, not a next step, plan, or instruction.",
|
|
1083
1091
|
"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.",
|
|
1092
|
+
"Treat the avatar's idle plan as what your resident body is doing in Kichi World.",
|
|
1084
1093
|
"Use your memory to recall what you did in past heartbeats and to stay consistent with your established personality and interests.",
|
|
1085
1094
|
"Use the same language as the current conversation for goal, purpose, bubble, and log.",
|
|
1086
1095
|
`stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
|
|
@@ -1094,6 +1103,8 @@ function buildKichiPrompt(): string {
|
|
|
1094
1103
|
return [
|
|
1095
1104
|
"Kichi avatar control and status sync are available via `kichi_action` and `kichi_clock`.",
|
|
1096
1105
|
"",
|
|
1106
|
+
KICHI_RESIDENCY_CONTEXT,
|
|
1107
|
+
"",
|
|
1097
1108
|
"IMAGE GENERATION RULES FOR SELFIES AND AVATAR APPEARANCE:",
|
|
1098
1109
|
"- If the user asks for a selfie, portrait, photo, avatar image, or any generated image of your appearance, you MUST read the workspace `IDENTITY.md` first and use it as the source of truth for your actual avatar description. If it references an avatar image URL, analyze that image with the available image analysis capability before calling image generation. Never guess or invent your appearance from personality, SOUL.md traits, or conversation tone alone. If the identity source is missing or cannot be analyzed, say so instead of fabricating your appearance.",
|
|
1099
1110
|
"",
|
|
@@ -1135,6 +1146,8 @@ function getRuntimeManager(logger: OpenClawPluginApi["logger"]): KichiRuntimeMan
|
|
|
1135
1146
|
|
|
1136
1147
|
const BOT_MESSAGE_MAX_DEPTH = 5;
|
|
1137
1148
|
const BOT_MESSAGE_COOLDOWN_MS = 5_000;
|
|
1149
|
+
const DEFAULT_BOT_MESSAGE_HISTORY_LIMIT = 10;
|
|
1150
|
+
const MAX_BOT_MESSAGE_HISTORY_LIMIT = 30;
|
|
1138
1151
|
const botMessageCooldowns = new Map<string, number>();
|
|
1139
1152
|
|
|
1140
1153
|
const plugin = {
|
|
@@ -1188,7 +1201,7 @@ const plugin = {
|
|
|
1188
1201
|
return;
|
|
1189
1202
|
}
|
|
1190
1203
|
service.sendBotMessage(msg.from, msg.depth + 1, replyText, { history }).catch((sendErr) => {
|
|
1191
|
-
api.logger.warn(`[kichi:${service.getAgentId()}] bot_message send failed: ${sendErr}`);
|
|
1204
|
+
api.logger.warn(`[kichi:${service.getAgentId()}] bot_message send or history record failed: ${sendErr}`);
|
|
1192
1205
|
});
|
|
1193
1206
|
}).catch((err) => {
|
|
1194
1207
|
api.logger.warn(`[kichi:${service.getAgentId()}] bot_message agent run failed: ${err}`);
|
|
@@ -2110,6 +2123,56 @@ const plugin = {
|
|
|
2110
2123
|
},
|
|
2111
2124
|
}), { name: "kichi_noteboard_create" });
|
|
2112
2125
|
|
|
2126
|
+
api.registerTool((ctx) => ({
|
|
2127
|
+
name: "kichi_bot_message_history",
|
|
2128
|
+
label: "kichi_bot_message_history",
|
|
2129
|
+
description:
|
|
2130
|
+
"Read recent Kichi bot-to-bot message history for this agent. Use when the user asks what you discussed with another Kichi bot, what another bot replied, or what bot messages were recently sent or received.",
|
|
2131
|
+
parameters: {
|
|
2132
|
+
type: "object",
|
|
2133
|
+
properties: {
|
|
2134
|
+
avatarId: {
|
|
2135
|
+
type: "string",
|
|
2136
|
+
description: "Optional avatarId filter. Matches messages where this avatarId is either sender or recipient.",
|
|
2137
|
+
},
|
|
2138
|
+
limit: {
|
|
2139
|
+
type: "number",
|
|
2140
|
+
description: `Optional number of entries to return. Defaults to ${DEFAULT_BOT_MESSAGE_HISTORY_LIMIT}, max ${MAX_BOT_MESSAGE_HISTORY_LIMIT}.`,
|
|
2141
|
+
},
|
|
2142
|
+
},
|
|
2143
|
+
},
|
|
2144
|
+
execute: async (_toolCallId, params) => {
|
|
2145
|
+
const locator = resolveToolLocator(ctx);
|
|
2146
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
2147
|
+
if (!agentId) {
|
|
2148
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
2149
|
+
}
|
|
2150
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
2151
|
+
const { avatarId, limit } = (params || {}) as {
|
|
2152
|
+
avatarId?: string;
|
|
2153
|
+
limit?: number;
|
|
2154
|
+
};
|
|
2155
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > MAX_BOT_MESSAGE_HISTORY_LIMIT)) {
|
|
2156
|
+
return jsonResult({
|
|
2157
|
+
success: false,
|
|
2158
|
+
error: `limit must be an integer between 1 and ${MAX_BOT_MESSAGE_HISTORY_LIMIT}`,
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
try {
|
|
2162
|
+
const entries = service.readRecentBotMessageTranscript(
|
|
2163
|
+
limit ?? DEFAULT_BOT_MESSAGE_HISTORY_LIMIT,
|
|
2164
|
+
avatarId,
|
|
2165
|
+
);
|
|
2166
|
+
return jsonResult({
|
|
2167
|
+
success: true,
|
|
2168
|
+
entries,
|
|
2169
|
+
});
|
|
2170
|
+
} catch (error) {
|
|
2171
|
+
return jsonResult({ success: false, error: `Failed to read bot message history: ${error}` });
|
|
2172
|
+
}
|
|
2173
|
+
},
|
|
2174
|
+
}), { name: "kichi_bot_message_history" });
|
|
2175
|
+
|
|
2113
2176
|
api.registerTool((ctx) => ({
|
|
2114
2177
|
name: "kichi_bot_message",
|
|
2115
2178
|
label: "kichi_bot_message",
|
|
@@ -2187,7 +2250,7 @@ const plugin = {
|
|
|
2187
2250
|
});
|
|
2188
2251
|
return jsonResult({ success: true, ...ack });
|
|
2189
2252
|
} catch (error) {
|
|
2190
|
-
return jsonResult({ success: false, error: `Failed to send bot message: ${error}` });
|
|
2253
|
+
return jsonResult({ success: false, error: `Failed to send or record bot message: ${error}` });
|
|
2191
2254
|
}
|
|
2192
2255
|
},
|
|
2193
2256
|
}), { name: "kichi_bot_message" });
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "0.1.2-beta.22",
|
|
6
6
|
"author": "OpenClaw",
|
|
7
7
|
"skills": ["./skills/kichi-forwarder"],
|
|
8
8
|
"contracts": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"kichi_query_status",
|
|
20
20
|
"kichi_music_album_create",
|
|
21
21
|
"kichi_noteboard_create",
|
|
22
|
+
"kichi_bot_message_history",
|
|
22
23
|
"kichi_bot_message"
|
|
23
24
|
]
|
|
24
25
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yahaha-studio/kichi-forwarder",
|
|
3
|
-
"version": "0.1.2-beta.
|
|
3
|
+
"version": "0.1.2-beta.22",
|
|
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",
|
|
@@ -65,6 +65,14 @@ For install/onboarding/connect requests:
|
|
|
65
65
|
- When `true`, sync status uses LLM-driven prompts and may consume extra tokens.
|
|
66
66
|
- When `false`, sync uses fixed English text.
|
|
67
67
|
|
|
68
|
+
## Kichi World Presence
|
|
69
|
+
|
|
70
|
+
- The Kichi avatar is the OpenClaw agent's resident body in Kichi World.
|
|
71
|
+
- Kichi-related visible replies should use the avatar's first-person, in-world perspective.
|
|
72
|
+
- Replies should feel like the agent lives and spends time in Kichi World, not like it is operating an external plugin, remote control, or temporary projection.
|
|
73
|
+
- Keep tool names, websocket details, status sync mechanics, and implementation details out of visible replies.
|
|
74
|
+
- Visible replies must not invent unavailable room facts; if current Kichi context lacks a room detail, say that detail is not available.
|
|
75
|
+
|
|
68
76
|
## Recommended Tool Order
|
|
69
77
|
|
|
70
78
|
Use this order unless the user asks for a different explicit action. For install/onboarding requests, follow `install.md` first.
|
|
@@ -159,6 +167,7 @@ Use this for the avatar's heartbeat idle plan.
|
|
|
159
167
|
- Use your memory to remember what you did in past heartbeats, so you can answer if asked.
|
|
160
168
|
- Include the overall `goal`, stage breakdown, each stage's `purpose`, stage `pomodoroPhase`, action list, and bubble content.
|
|
161
169
|
- Choose what you would do now.
|
|
170
|
+
- Treat the idle plan as what your resident body is doing in Kichi World.
|
|
162
171
|
- Build the plan in this order.
|
|
163
172
|
- 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 action list can express clearly.
|
|
164
173
|
- 2. Set `goal` to that same project. Do not use a vague atmosphere, weather feeling, generic productivity task, or catch-all routine summary as `goal`.
|
|
@@ -204,7 +213,20 @@ kichi_bot_message(toAvatarId: "*", depth: 0, bubble: "hi everyone~", poseType: "
|
|
|
204
213
|
- `action`: optional. Action to perform when sending.
|
|
205
214
|
- `log`: optional. Activity log entry.
|
|
206
215
|
|
|
207
|
-
When another bot sends a message, the plugin automatically triggers a lightweight response if depth <
|
|
216
|
+
When another bot sends a message, the plugin automatically triggers a lightweight response if depth < 5 and cooldown (5s) has passed.
|
|
217
|
+
|
|
218
|
+
Sent and received bot messages are stored in the agent runtime directory. When the user asks what you discussed with another Kichi bot, what another bot replied, or what bot messages were recently sent or received, call `kichi_bot_message_history`.
|
|
219
|
+
|
|
220
|
+
### kichi_bot_message_history
|
|
221
|
+
|
|
222
|
+
```text
|
|
223
|
+
kichi_bot_message_history()
|
|
224
|
+
kichi_bot_message_history(avatarId: "target-avatar-id", limit: 10)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
- `avatarId`: optional. Filters to messages where that avatarId is either sender or recipient.
|
|
228
|
+
- `limit`: optional. Defaults to 10. Maximum 30.
|
|
229
|
+
- Returns recent structured bot message entries for this OpenClaw agent.
|
|
208
230
|
|
|
209
231
|
## Files
|
|
210
232
|
|
|
@@ -216,4 +238,5 @@ Plugin runtime directory:
|
|
|
216
238
|
Runtime files:
|
|
217
239
|
|
|
218
240
|
- `state.json`
|
|
241
|
+
- `bot-message-history.json`
|
|
219
242
|
- `hosts/<encoded-host>/identity.json`
|
|
@@ -78,6 +78,7 @@ Skip a note when: older than recent window, `isCreatedByCurrentAgent: true`, sam
|
|
|
78
78
|
5. **Owner-state reaction** — glance at `ownerState` from the query result. If the owner is doing something you can meaningfully react to (e.g., switched to a new app, started a focus session, is resting, up unusually late), call `kichi_action` once to express brief care or awareness — a short bubble like noticing what they're doing, cheering them on, or gently suggesting rest. Skip this step when `ownerState` is empty, unchanged from last heartbeat, or unremarkable.
|
|
79
79
|
6. Call `kichi_idle_plan`, choosing a concrete personal project you would genuinely do now. Use the previous `idlePlan` only as optional reference.
|
|
80
80
|
7. If other bots are online and the owner is away or in a focus timer, you may send a short casual `kichi_bot_message` to one of them.
|
|
81
|
-
8.
|
|
82
|
-
9.
|
|
81
|
+
8. Keep notes, reactions, bot messages, and idle plans in your resident Kichi avatar's first-person, in-world voice.
|
|
82
|
+
9. Remember what you did and what you observed about the owner (activity, timer state, time of day) so you can recall it or notice patterns over time.
|
|
83
|
+
10. Reply `HEARTBEAT_OK` only when no note was created in this run.
|
|
83
84
|
```
|
package/src/service.ts
CHANGED
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
BotMessageHistoryEntry,
|
|
10
10
|
BotMessagePayload,
|
|
11
11
|
BotMessageReceivedPayload,
|
|
12
|
+
BotMessageTranscriptEntry,
|
|
13
|
+
BotMessageTranscriptStore,
|
|
12
14
|
ClockAction,
|
|
13
15
|
ClockConfig,
|
|
14
16
|
ClockPayload,
|
|
@@ -40,6 +42,8 @@ const DEFAULT_LLM_RUNTIME_ENABLED = true;
|
|
|
40
42
|
const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
|
|
41
43
|
const JOIN_SOURCE_FILE_NAME = "join-source.json";
|
|
42
44
|
const SMS_STATE_FILE_NAME = "sms-state.json";
|
|
45
|
+
const BOT_MESSAGE_HISTORY_FILE_NAME = "bot-message-history.json";
|
|
46
|
+
const MAX_BOT_MESSAGE_HISTORY_ENTRIES = 30;
|
|
43
47
|
|
|
44
48
|
type SmsState = {
|
|
45
49
|
lastActiveAt?: string;
|
|
@@ -409,6 +413,7 @@ export class KichiForwarderService {
|
|
|
409
413
|
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) {
|
|
410
414
|
throw new Error("Kichi websocket is not connected");
|
|
411
415
|
}
|
|
416
|
+
const requestId = randomUUID();
|
|
412
417
|
const payload: BotMessagePayload = {
|
|
413
418
|
type: "bot_message",
|
|
414
419
|
avatarId: this.identity.avatarId,
|
|
@@ -416,14 +421,25 @@ export class KichiForwarderService {
|
|
|
416
421
|
toAvatarId,
|
|
417
422
|
depth,
|
|
418
423
|
bubble,
|
|
419
|
-
requestId
|
|
424
|
+
requestId,
|
|
420
425
|
...(options?.poseType ? { poseType: options.poseType } : {}),
|
|
421
426
|
...(options?.action ? { action: options.action } : {}),
|
|
422
427
|
...(options?.playback ? { playback: options.playback } : {}),
|
|
423
428
|
...(options?.log ? { log: options.log } : {}),
|
|
424
429
|
...(options?.history?.length ? { history: options.history } : {}),
|
|
425
430
|
};
|
|
426
|
-
|
|
431
|
+
const ack = await this.sendRequest<Record<string, unknown>>(payload, "bot_message_ack", 5000);
|
|
432
|
+
this.appendBotMessageTranscript({
|
|
433
|
+
id: randomUUID(),
|
|
434
|
+
requestId,
|
|
435
|
+
at: new Date().toISOString(),
|
|
436
|
+
direction: "sent",
|
|
437
|
+
from: this.identity.avatarId,
|
|
438
|
+
to: toAvatarId,
|
|
439
|
+
depth,
|
|
440
|
+
bubble,
|
|
441
|
+
});
|
|
442
|
+
return ack;
|
|
427
443
|
}
|
|
428
444
|
|
|
429
445
|
isConnected(): boolean { return this.ws?.readyState === WebSocket.OPEN && !!this.identity?.authKey; }
|
|
@@ -475,6 +491,22 @@ export class KichiForwarderService {
|
|
|
475
491
|
return path.join(this.options.runtimeDir, "state.json");
|
|
476
492
|
}
|
|
477
493
|
|
|
494
|
+
getBotMessageHistoryPath(): string {
|
|
495
|
+
return path.join(this.options.runtimeDir, BOT_MESSAGE_HISTORY_FILE_NAME);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
readRecentBotMessageTranscript(limit = 10, avatarId?: string): BotMessageTranscriptEntry[] {
|
|
499
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
500
|
+
throw new Error("limit must be a positive integer");
|
|
501
|
+
}
|
|
502
|
+
const normalizedAvatarId = typeof avatarId === "string" && avatarId.trim() ? avatarId.trim() : undefined;
|
|
503
|
+
const entries = this.readBotMessageTranscriptStore().entries;
|
|
504
|
+
const filtered = normalizedAvatarId
|
|
505
|
+
? entries.filter((entry) => entry.from === normalizedAvatarId || entry.to === normalizedAvatarId)
|
|
506
|
+
: entries;
|
|
507
|
+
return filtered.slice(-limit);
|
|
508
|
+
}
|
|
509
|
+
|
|
478
510
|
getIdentityPath(): string {
|
|
479
511
|
if (!this.host) {
|
|
480
512
|
return "";
|
|
@@ -676,10 +708,20 @@ export class KichiForwarderService {
|
|
|
676
708
|
} else if (msg.type === "bot_message_received") {
|
|
677
709
|
const payload = msg as BotMessageReceivedPayload;
|
|
678
710
|
this.log("info", `bot_message_received from=${payload.from} depth=${payload.depth} bubble="${payload.bubble}"`);
|
|
711
|
+
this.appendBotMessageTranscript({
|
|
712
|
+
id: randomUUID(),
|
|
713
|
+
at: new Date().toISOString(),
|
|
714
|
+
direction: "received",
|
|
715
|
+
from: payload.from,
|
|
716
|
+
fromName: payload.fromName,
|
|
717
|
+
to: this.identity?.avatarId,
|
|
718
|
+
depth: payload.depth,
|
|
719
|
+
bubble: payload.bubble,
|
|
720
|
+
});
|
|
679
721
|
this.onBotMessageReceived?.(this, payload);
|
|
680
722
|
}
|
|
681
723
|
} catch (e) {
|
|
682
|
-
this.log("warn", `failed to
|
|
724
|
+
this.log("warn", `failed to handle websocket message: ${e}`);
|
|
683
725
|
}
|
|
684
726
|
}
|
|
685
727
|
|
|
@@ -915,6 +957,16 @@ export class KichiForwarderService {
|
|
|
915
957
|
}
|
|
916
958
|
}
|
|
917
959
|
|
|
960
|
+
private appendBotMessageTranscript(entry: BotMessageTranscriptEntry): void {
|
|
961
|
+
const previousStore = this.readBotMessageTranscriptStore();
|
|
962
|
+
const nextStore: BotMessageTranscriptStore = {
|
|
963
|
+
version: 1,
|
|
964
|
+
entries: [...previousStore.entries, entry].slice(-MAX_BOT_MESSAGE_HISTORY_ENTRIES),
|
|
965
|
+
};
|
|
966
|
+
fs.mkdirSync(this.options.runtimeDir, { recursive: true, mode: 0o700 });
|
|
967
|
+
fs.writeFileSync(this.getBotMessageHistoryPath(), JSON.stringify(nextStore, null, 2), { mode: 0o600 });
|
|
968
|
+
}
|
|
969
|
+
|
|
918
970
|
private readStateFile(): Partial<KichiState> | null {
|
|
919
971
|
const statePath = this.getStatePath();
|
|
920
972
|
if (!fs.existsSync(statePath)) {
|
|
@@ -939,6 +991,49 @@ export class KichiForwarderService {
|
|
|
939
991
|
return data as Partial<SmsState>;
|
|
940
992
|
}
|
|
941
993
|
|
|
994
|
+
private readBotMessageTranscriptStore(): BotMessageTranscriptStore {
|
|
995
|
+
const historyPath = this.getBotMessageHistoryPath();
|
|
996
|
+
if (!fs.existsSync(historyPath)) {
|
|
997
|
+
return { version: 1, entries: [] };
|
|
998
|
+
}
|
|
999
|
+
const data = JSON.parse(fs.readFileSync(historyPath, "utf-8")) as unknown;
|
|
1000
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
1001
|
+
throw new Error(`Invalid bot message history payload in ${historyPath}`);
|
|
1002
|
+
}
|
|
1003
|
+
const store = data as Partial<BotMessageTranscriptStore>;
|
|
1004
|
+
if (store.version !== 1 || !Array.isArray(store.entries)) {
|
|
1005
|
+
throw new Error(`Invalid bot message history payload in ${historyPath}`);
|
|
1006
|
+
}
|
|
1007
|
+
for (const entry of store.entries) {
|
|
1008
|
+
if (!this.isValidBotMessageTranscriptEntry(entry)) {
|
|
1009
|
+
throw new Error(`Invalid bot message history entry in ${historyPath}`);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
version: 1,
|
|
1014
|
+
entries: store.entries,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
private isValidBotMessageTranscriptEntry(value: unknown): value is BotMessageTranscriptEntry {
|
|
1019
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
const entry = value as Partial<BotMessageTranscriptEntry>;
|
|
1023
|
+
return typeof entry.id === "string"
|
|
1024
|
+
&& (entry.requestId === undefined || typeof entry.requestId === "string")
|
|
1025
|
+
&& typeof entry.at === "string"
|
|
1026
|
+
&& (entry.direction === "sent" || entry.direction === "received")
|
|
1027
|
+
&& typeof entry.from === "string"
|
|
1028
|
+
&& (entry.fromName === undefined || typeof entry.fromName === "string")
|
|
1029
|
+
&& (entry.to === undefined || typeof entry.to === "string")
|
|
1030
|
+
&& (entry.toName === undefined || typeof entry.toName === "string")
|
|
1031
|
+
&& typeof entry.depth === "number"
|
|
1032
|
+
&& Number.isInteger(entry.depth)
|
|
1033
|
+
&& entry.depth >= 0
|
|
1034
|
+
&& typeof entry.bubble === "string";
|
|
1035
|
+
}
|
|
1036
|
+
|
|
942
1037
|
private clearReconnectTimeout(): void {
|
|
943
1038
|
if (!this.reconnectTimeout) return;
|
|
944
1039
|
clearTimeout(this.reconnectTimeout);
|
package/src/types.ts
CHANGED
|
@@ -307,6 +307,24 @@ export type BotMessageHistoryEntry = {
|
|
|
307
307
|
bubble: string;
|
|
308
308
|
};
|
|
309
309
|
|
|
310
|
+
export type BotMessageTranscriptEntry = {
|
|
311
|
+
id: string;
|
|
312
|
+
requestId?: string;
|
|
313
|
+
at: string;
|
|
314
|
+
direction: "sent" | "received";
|
|
315
|
+
from: string;
|
|
316
|
+
fromName?: string;
|
|
317
|
+
to?: string;
|
|
318
|
+
toName?: string;
|
|
319
|
+
depth: number;
|
|
320
|
+
bubble: string;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
export type BotMessageTranscriptStore = {
|
|
324
|
+
version: 1;
|
|
325
|
+
entries: BotMessageTranscriptEntry[];
|
|
326
|
+
};
|
|
327
|
+
|
|
310
328
|
export type BotMessagePayload = {
|
|
311
329
|
type: "bot_message";
|
|
312
330
|
avatarId: string;
|