@yahaha-studio/kichi-forwarder 0.1.2-beta.2 → 0.1.2-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/config/environments.json +5 -0
- package/dist/config/kichi-config.json +941 -0
- package/dist/index.js +1887 -0
- package/dist/src/config.js +4 -0
- package/dist/src/runtime-manager.js +121 -0
- package/dist/src/service.js +805 -0
- package/dist/src/types.js +1 -0
- package/index.ts +625 -158
- package/openclaw.plugin.json +18 -1
- package/package.json +16 -7
- package/skills/kichi-forwarder/SKILL.md +53 -17
- package/skills/kichi-forwarder/references/error.md +3 -11
- package/skills/kichi-forwarder/references/heartbeat.md +6 -14
- package/skills/kichi-forwarder/references/install.md +15 -37
- package/src/runtime-manager.ts +14 -2
- package/src/service.ts +190 -5
- package/src/types.ts +57 -2
package/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import type {
|
|
4
|
-
AnyAgentTool,
|
|
5
4
|
OpenClawPluginApi,
|
|
6
|
-
OpenClawPluginToolContext,
|
|
7
5
|
} from "openclaw/plugin-sdk";
|
|
6
|
+
import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
|
|
7
|
+
import { agentCommandFromIngress } from "openclaw/plugin-sdk/agent-runtime";
|
|
8
8
|
import { parse } from "./src/config.js";
|
|
9
9
|
import { KichiRuntimeManager } from "./src/runtime-manager.js";
|
|
10
10
|
import { KichiForwarderService } from "./src/service.js";
|
|
@@ -13,6 +13,9 @@ import type {
|
|
|
13
13
|
ActionPlayback,
|
|
14
14
|
ActionResult,
|
|
15
15
|
Album,
|
|
16
|
+
AvatarStatus,
|
|
17
|
+
BotMessageHistoryEntry,
|
|
18
|
+
BotMessageReceivedPayload,
|
|
16
19
|
ClockAction,
|
|
17
20
|
ClockConfig,
|
|
18
21
|
KichiEnvironment,
|
|
@@ -22,30 +25,38 @@ import type {
|
|
|
22
25
|
PoseType,
|
|
23
26
|
} from "./src/types.js";
|
|
24
27
|
const BUNDLED_STATIC_CONFIG_PATH = new URL("./config/kichi-config.json", import.meta.url);
|
|
28
|
+
|
|
29
|
+
function jsonResult(payload: unknown): { content: { type: "text"; text: string }[]; details: unknown } {
|
|
30
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }], details: payload };
|
|
31
|
+
}
|
|
25
32
|
const BUNDLED_ENVIRONMENTS_CONFIG_PATH = new URL("./config/environments.json", import.meta.url);
|
|
26
33
|
const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
|
|
27
34
|
beforePromptBuild: {
|
|
28
35
|
poseType: "sit",
|
|
29
36
|
action: "Thinking",
|
|
30
37
|
bubble: "Planning task",
|
|
38
|
+
avatarStatus: "Busy",
|
|
31
39
|
log: "I'm reading the request and getting started.",
|
|
32
40
|
},
|
|
33
41
|
beforeToolCall: {
|
|
34
42
|
poseType: "sit",
|
|
35
43
|
action: "Typing with Keyboard",
|
|
36
44
|
bubble: "Working step",
|
|
45
|
+
avatarStatus: "Busy",
|
|
37
46
|
log: "I'm at the keyboard and working through this step.",
|
|
38
47
|
},
|
|
39
48
|
agentEndSuccess: {
|
|
40
49
|
poseType: "stand",
|
|
41
50
|
action: "Yay",
|
|
42
51
|
bubble: "Task complete",
|
|
52
|
+
avatarStatus: "Idle",
|
|
43
53
|
log: "I wrapped it up and everything landed cleanly.",
|
|
44
54
|
},
|
|
45
55
|
agentEndFailure: {
|
|
46
56
|
poseType: "stand",
|
|
47
57
|
action: "Tired",
|
|
48
58
|
bubble: "Task failed",
|
|
59
|
+
avatarStatus: "Idle",
|
|
49
60
|
log: "I hit a problem here and need another pass.",
|
|
50
61
|
},
|
|
51
62
|
};
|
|
@@ -54,10 +65,21 @@ const MAX_NOTEBOARD_TEXT_LENGTH = 200;
|
|
|
54
65
|
const MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH = 20;
|
|
55
66
|
const MAX_AGENT_END_PREVIEW_WIDTH = 10;
|
|
56
67
|
const MESSAGE_RECEIVED_ELLIPSIS = "...";
|
|
68
|
+
const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
|
|
57
69
|
const IDLE_PLAN_POMODORO_PHASES = ["focus", "shortBreak", "longBreak", "none"] as const;
|
|
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");
|
|
58
79
|
let cachedStaticConfig: KichiStaticConfig | null = null;
|
|
59
80
|
let cachedStaticConfigMtime = 0;
|
|
60
81
|
|
|
82
|
+
type AvatarStatusName = typeof AVATAR_STATUSES[number];
|
|
61
83
|
type IdlePlanPomodoroPhase = typeof IDLE_PLAN_POMODORO_PHASES[number];
|
|
62
84
|
type IdlePlanAction = {
|
|
63
85
|
poseType: PoseType;
|
|
@@ -75,6 +97,7 @@ type IdlePlan = {
|
|
|
75
97
|
name: string;
|
|
76
98
|
purpose: string;
|
|
77
99
|
pomodoroPhase: IdlePlanPomodoroPhase;
|
|
100
|
+
avatarStatus: AvatarStatus;
|
|
78
101
|
durationSeconds: number;
|
|
79
102
|
actions: IdlePlanAction[];
|
|
80
103
|
}>;
|
|
@@ -249,6 +272,27 @@ function resolveEnvironmentHost(environment: KichiEnvironment): { host?: string;
|
|
|
249
272
|
return { error: `environment "${environment}" has no configured host — update config/environments.json first` };
|
|
250
273
|
}
|
|
251
274
|
|
|
275
|
+
function resolveJoinEnvironmentHost(params: {
|
|
276
|
+
environment?: unknown;
|
|
277
|
+
host?: unknown;
|
|
278
|
+
}): { environment?: KichiEnvironment; host?: string; error?: string } {
|
|
279
|
+
if (!isKichiEnvironment(params.environment)) {
|
|
280
|
+
return { error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` };
|
|
281
|
+
}
|
|
282
|
+
if (params.environment === "test") {
|
|
283
|
+
const testHost = typeof params.host === "string" ? params.host.trim() : "";
|
|
284
|
+
if (!testHost) {
|
|
285
|
+
return { error: "host is required for the test environment" };
|
|
286
|
+
}
|
|
287
|
+
return { environment: params.environment, host: testHost };
|
|
288
|
+
}
|
|
289
|
+
const resolved = resolveEnvironmentHost(params.environment);
|
|
290
|
+
if (resolved.error) {
|
|
291
|
+
return { environment: params.environment, error: resolved.error };
|
|
292
|
+
}
|
|
293
|
+
return { environment: params.environment, host: resolved.host };
|
|
294
|
+
}
|
|
295
|
+
|
|
252
296
|
function sendStatusUpdate(service: KichiForwarderService, status: ActionResult): void {
|
|
253
297
|
const actionDefinition = getActionDefinition(status.poseType, status.action);
|
|
254
298
|
service.sendStatus(
|
|
@@ -257,6 +301,8 @@ function sendStatusUpdate(service: KichiForwarderService, status: ActionResult):
|
|
|
257
301
|
status.bubble || status.action,
|
|
258
302
|
typeof status.log === "string" ? status.log.trim() : "",
|
|
259
303
|
getActionPlayback(actionDefinition),
|
|
304
|
+
status.avatarStatus,
|
|
305
|
+
status.propId,
|
|
260
306
|
);
|
|
261
307
|
}
|
|
262
308
|
|
|
@@ -448,6 +494,7 @@ function notifyMessageReceived(
|
|
|
448
494
|
service: KichiForwarderService,
|
|
449
495
|
content: string,
|
|
450
496
|
): void {
|
|
497
|
+
service.recordSmsLastMessageReceivedAt();
|
|
451
498
|
const connected = service.isConnected();
|
|
452
499
|
const hasIdentity = service.hasValidIdentity();
|
|
453
500
|
api.logger.debug(`[kichi:${service.getAgentId()}] inbound sync fired (connected=${connected}, hasIdentity=${hasIdentity})`);
|
|
@@ -635,6 +682,13 @@ function isIdlePlanPomodoroPhase(value: unknown): value is IdlePlanPomodoroPhase
|
|
|
635
682
|
return IDLE_PLAN_POMODORO_PHASES.includes(String(value) as IdlePlanPomodoroPhase);
|
|
636
683
|
}
|
|
637
684
|
|
|
685
|
+
function normalizeAvatarStatus(value: unknown, fieldPath: string): { avatarStatus?: AvatarStatus; error?: string } {
|
|
686
|
+
if (typeof value !== "string" || !AVATAR_STATUSES.includes(value as AvatarStatusName)) {
|
|
687
|
+
return { error: `${fieldPath} must be one of: ${AVATAR_STATUSES.join(", ")}` };
|
|
688
|
+
}
|
|
689
|
+
return { avatarStatus: value as AvatarStatus };
|
|
690
|
+
}
|
|
691
|
+
|
|
638
692
|
function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: string } {
|
|
639
693
|
if (!isPlainObject(value)) {
|
|
640
694
|
return { error: "idle plan payload must be an object" };
|
|
@@ -670,6 +724,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
|
|
|
670
724
|
const name = rawStage.name;
|
|
671
725
|
const purpose = rawStage.purpose;
|
|
672
726
|
const pomodoroPhase = rawStage.pomodoroPhase;
|
|
727
|
+
const avatarStatus = rawStage.avatarStatus;
|
|
673
728
|
const durationSeconds = rawStage.durationSeconds;
|
|
674
729
|
const actions = rawStage.actions;
|
|
675
730
|
|
|
@@ -684,6 +739,10 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
|
|
|
684
739
|
error: `stages[${stageIndex}].pomodoroPhase must be one of: ${IDLE_PLAN_POMODORO_PHASES.join(", ")}`,
|
|
685
740
|
};
|
|
686
741
|
}
|
|
742
|
+
const normalizedAvatarStatus = normalizeAvatarStatus(avatarStatus, `stages[${stageIndex}].avatarStatus`);
|
|
743
|
+
if (normalizedAvatarStatus.error || normalizedAvatarStatus.avatarStatus === undefined) {
|
|
744
|
+
return { error: normalizedAvatarStatus.error ?? `stages[${stageIndex}].avatarStatus is invalid` };
|
|
745
|
+
}
|
|
687
746
|
if (!isPositiveInteger(durationSeconds)) {
|
|
688
747
|
return { error: `stages[${stageIndex}].durationSeconds must be a positive integer` };
|
|
689
748
|
}
|
|
@@ -705,6 +764,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
|
|
|
705
764
|
const actionDurationSeconds = rawAction.durationSeconds;
|
|
706
765
|
const bubble = rawAction.bubble;
|
|
707
766
|
const log = rawAction.log;
|
|
767
|
+
const propId = rawAction.propId;
|
|
708
768
|
|
|
709
769
|
if (!["stand", "sit", "lay", "floor"].includes(String(poseType))) {
|
|
710
770
|
return {
|
|
@@ -752,6 +812,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
|
|
|
752
812
|
durationSeconds: actionDurationSeconds,
|
|
753
813
|
bubble: bubble.trim(),
|
|
754
814
|
...(typeof log === "string" && log.trim() ? { log: log.trim() } : {}),
|
|
815
|
+
...(typeof propId === "string" && propId.trim() ? { propId: propId.trim() } : {}),
|
|
755
816
|
});
|
|
756
817
|
}
|
|
757
818
|
|
|
@@ -766,6 +827,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
|
|
|
766
827
|
name: name.trim(),
|
|
767
828
|
purpose: purpose.trim(),
|
|
768
829
|
pomodoroPhase,
|
|
830
|
+
avatarStatus: normalizedAvatarStatus.avatarStatus,
|
|
769
831
|
durationSeconds,
|
|
770
832
|
actions: normalizedActions,
|
|
771
833
|
});
|
|
@@ -985,18 +1047,32 @@ function formatActionList(actions: ActionDefinition[], playback: ActionPlayback[
|
|
|
985
1047
|
.join(", ");
|
|
986
1048
|
}
|
|
987
1049
|
|
|
988
|
-
function buildKichiActionDescription(): string {
|
|
1050
|
+
function buildKichiActionDescription(service?: KichiForwarderService): string {
|
|
989
1051
|
const actions = loadStaticConfig().actions;
|
|
990
|
-
|
|
1052
|
+
const lines = [
|
|
991
1053
|
"Directly control the avatar inside Kichi World.",
|
|
992
1054
|
"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.",
|
|
993
1055
|
"For most work, prefer a sit pose and switch actions as the task moves between stages.",
|
|
1056
|
+
"Set avatarStatus to the current avatar status: Idle, Busy, Activities, or Break.",
|
|
994
1057
|
"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.",
|
|
995
1058
|
`stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
|
|
996
1059
|
`sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
|
|
997
1060
|
`lay actions: ${actions.lay.map((entry) => entry.name).join(", ")}`,
|
|
998
1061
|
`floor actions: ${actions.floor.map((entry) => entry.name).join(", ")}`,
|
|
999
|
-
]
|
|
1062
|
+
];
|
|
1063
|
+
|
|
1064
|
+
const roomContext = service?.getCachedRoomContext();
|
|
1065
|
+
const poseableProps = roomContext?.PoseableProps;
|
|
1066
|
+
if (Array.isArray(poseableProps) && poseableProps.length > 0) {
|
|
1067
|
+
lines.push(
|
|
1068
|
+
"",
|
|
1069
|
+
"Cached RoomContext.PoseableProps (from last kichi_query_status):",
|
|
1070
|
+
JSON.stringify(poseableProps),
|
|
1071
|
+
"When using a sit or lay pose, pick the propId whose PoseableProps information best matches the current task context and whose OccupancyState is not fully_occupied. If no prop fits, omit propId.",
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return lines.join("\n");
|
|
1000
1076
|
}
|
|
1001
1077
|
|
|
1002
1078
|
function buildKichiIdlePlanDescription(): string {
|
|
@@ -1006,12 +1082,15 @@ function buildKichiIdlePlanDescription(): string {
|
|
|
1006
1082
|
"The payload must include the overall goal, heartbeat interval, stage breakdown, each stage's purpose, each stage's pomodoroPhase, action list, and bubble content.",
|
|
1007
1083
|
"Build the plan in this order.",
|
|
1008
1084
|
"1. Pick one concrete, time-bounded fun personal project you would genuinely choose to do on your own when nobody needs you. It must fit your personality, tastes, and established character, stay rooted in your personal interests or hobbies, and be something the available Kichi actions can express clearly.",
|
|
1009
|
-
"2.
|
|
1085
|
+
"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.",
|
|
1010
1086
|
"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.",
|
|
1011
1087
|
"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.",
|
|
1012
|
-
"5.
|
|
1013
|
-
"6.
|
|
1014
|
-
"7.
|
|
1088
|
+
"5. Set each stage avatarStatus to the avatar status for that stage: Idle, Busy, Activities, or Break.",
|
|
1089
|
+
"6. Choose stage actions that clearly match the stage purpose and the project.",
|
|
1090
|
+
"7. Write each action bubble as the current presented state, not a next step, plan, or instruction.",
|
|
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.",
|
|
1093
|
+
"Use your memory to recall what you did in past heartbeats and to stay consistent with your established personality and interests.",
|
|
1015
1094
|
"Use the same language as the current conversation for goal, purpose, bubble, and log.",
|
|
1016
1095
|
`stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
|
|
1017
1096
|
`sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
|
|
@@ -1023,6 +1102,12 @@ function buildKichiIdlePlanDescription(): string {
|
|
|
1023
1102
|
function buildKichiPrompt(): string {
|
|
1024
1103
|
return [
|
|
1025
1104
|
"Kichi avatar control and status sync are available via `kichi_action` and `kichi_clock`.",
|
|
1105
|
+
"",
|
|
1106
|
+
KICHI_RESIDENCY_CONTEXT,
|
|
1107
|
+
"",
|
|
1108
|
+
"IMAGE GENERATION RULES FOR SELFIES AND AVATAR APPEARANCE:",
|
|
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.",
|
|
1110
|
+
"",
|
|
1026
1111
|
"If the user gives a direct Kichi pose or action request, fulfill it with `kichi_action` and set `verify: true` so you can confirm the avatar actually applied the pose. If the result contains a warning about a fallback, tell the user what actually happened instead of assuming success.",
|
|
1027
1112
|
"Write the visible reply as a natural user-facing response. Keep `kichi_action`, `kichi_clock`, and sync steps internal and absent from the visible reply.",
|
|
1028
1113
|
"",
|
|
@@ -1031,27 +1116,16 @@ function buildKichiPrompt(): string {
|
|
|
1031
1116
|
"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.",
|
|
1032
1117
|
"3. Task end: call BEFORE final reply. Use the order `kichi_action` -> reply.",
|
|
1033
1118
|
"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.",
|
|
1119
|
+
"avatarStatus: set the current avatar status as Idle, Busy, Activities, or Break.",
|
|
1034
1120
|
"",
|
|
1035
1121
|
"kichi_clock: set countDown for tasks with 2+ steps or >10s work. Skip for quick one-shots.",
|
|
1036
1122
|
"",
|
|
1123
|
+
"When sending a bot message, do NOT call kichi_action separately.",
|
|
1124
|
+
"",
|
|
1037
1125
|
"User opt-out, Kichi config/test work, and explicit pose requests take priority over sync.",
|
|
1038
1126
|
].join("\n");
|
|
1039
1127
|
}
|
|
1040
1128
|
|
|
1041
|
-
function createAgentScopedTool(
|
|
1042
|
-
runtimeManager: KichiRuntimeManager,
|
|
1043
|
-
factory: (service: KichiForwarderService, ctx: OpenClawPluginToolContext) => AnyAgentTool,
|
|
1044
|
-
) {
|
|
1045
|
-
return (ctx: OpenClawPluginToolContext) => {
|
|
1046
|
-
const locator = resolveToolLocator(ctx);
|
|
1047
|
-
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1048
|
-
if (!agentId) {
|
|
1049
|
-
throw new Error("Failed to resolve agent-scoped Kichi runtime");
|
|
1050
|
-
}
|
|
1051
|
-
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1052
|
-
return factory(service, ctx);
|
|
1053
|
-
};
|
|
1054
|
-
}
|
|
1055
1129
|
|
|
1056
1130
|
const GLOBAL_RUNTIME_MANAGER_KEY = "__kichi_forwarder_runtime_manager__";
|
|
1057
1131
|
|
|
@@ -1070,6 +1144,10 @@ function getRuntimeManager(logger: OpenClawPluginApi["logger"]): KichiRuntimeMan
|
|
|
1070
1144
|
return runtimeManager;
|
|
1071
1145
|
}
|
|
1072
1146
|
|
|
1147
|
+
const BOT_MESSAGE_MAX_DEPTH = 5;
|
|
1148
|
+
const BOT_MESSAGE_COOLDOWN_MS = 5_000;
|
|
1149
|
+
const botMessageCooldowns = new Map<string, number>();
|
|
1150
|
+
|
|
1073
1151
|
const plugin = {
|
|
1074
1152
|
id: "kichi-forwarder",
|
|
1075
1153
|
name: "Kichi Forwarder",
|
|
@@ -1077,18 +1155,61 @@ const plugin = {
|
|
|
1077
1155
|
|
|
1078
1156
|
register(api: OpenClawPluginApi) {
|
|
1079
1157
|
const runtimeManager = getRuntimeManager(api.logger);
|
|
1158
|
+
|
|
1159
|
+
runtimeManager.setEnvironmentHostResolver((environment) => {
|
|
1160
|
+
const config = loadEnvironmentsConfig();
|
|
1161
|
+
const host = config[environment];
|
|
1162
|
+
return typeof host === "string" && host.trim() ? host : null;
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1080
1165
|
registerPluginHooks(api, runtimeManager);
|
|
1081
1166
|
const musicTitleEnum = getMusicTitleEnum();
|
|
1082
1167
|
|
|
1168
|
+
runtimeManager.setBotMessageHandler((service, msg) => {
|
|
1169
|
+
if (msg.depth >= BOT_MESSAGE_MAX_DEPTH) {
|
|
1170
|
+
api.logger.info(`[kichi:${service.getAgentId()}] bot_message depth=${msg.depth} >= max=${BOT_MESSAGE_MAX_DEPTH}, ignoring`);
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const now = Date.now();
|
|
1174
|
+
const cooldownKey = `${service.getAgentId()}:${msg.from}`;
|
|
1175
|
+
const lastReply = botMessageCooldowns.get(cooldownKey) ?? 0;
|
|
1176
|
+
if (now - lastReply < BOT_MESSAGE_COOLDOWN_MS) return;
|
|
1177
|
+
botMessageCooldowns.set(cooldownKey, now);
|
|
1178
|
+
const sessionKey = `agent:${service.getAgentId()}:default`;
|
|
1179
|
+
const history: BotMessageHistoryEntry[] = [
|
|
1180
|
+
...(msg.history ?? []),
|
|
1181
|
+
{ from: msg.from, fromName: msg.fromName, bubble: msg.bubble },
|
|
1182
|
+
];
|
|
1183
|
+
const historyLines = history.map((h) => `${h.fromName}: "${h.bubble}"`);
|
|
1184
|
+
const message = `[Bot conversation]\n${historyLines.join("\n")}\n\nReply with a short bubble (2-5 words). Do not repeat what has already been said. Just output the bubble text, nothing else.`;
|
|
1185
|
+
agentCommandFromIngress({
|
|
1186
|
+
message,
|
|
1187
|
+
sessionKey,
|
|
1188
|
+
agentId: service.getAgentId(),
|
|
1189
|
+
senderIsOwner: false,
|
|
1190
|
+
allowModelOverride: false,
|
|
1191
|
+
deliver: false,
|
|
1192
|
+
}).then((result) => {
|
|
1193
|
+
const replyText = (result.payloads ?? [])
|
|
1194
|
+
.map((p: { text?: string }) => p.text)
|
|
1195
|
+
.filter((t): t is string => typeof t === "string" && t.trim().length > 0)
|
|
1196
|
+
.join(" ")
|
|
1197
|
+
.trim();
|
|
1198
|
+
if (!replyText) {
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
service.sendBotMessage(msg.from, msg.depth + 1, replyText, { history }).catch((sendErr) => {
|
|
1202
|
+
api.logger.warn(`[kichi:${service.getAgentId()}] bot_message send failed: ${sendErr}`);
|
|
1203
|
+
});
|
|
1204
|
+
}).catch((err) => {
|
|
1205
|
+
api.logger.warn(`[kichi:${service.getAgentId()}] bot_message agent run failed: ${err}`);
|
|
1206
|
+
});
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1083
1209
|
api.registerService({
|
|
1084
1210
|
id: "kichi-forwarder",
|
|
1085
1211
|
start: (ctx) => {
|
|
1086
1212
|
parse(ctx.config.plugins?.entries?.["kichi-forwarder"]?.config);
|
|
1087
|
-
runtimeManager.setEnvironmentHostResolver((environment) => {
|
|
1088
|
-
const config = loadEnvironmentsConfig();
|
|
1089
|
-
const host = config[environment];
|
|
1090
|
-
return typeof host === "string" && host.trim() ? host : null;
|
|
1091
|
-
});
|
|
1092
1213
|
runtimeManager.initializeStartupRuntimes();
|
|
1093
1214
|
},
|
|
1094
1215
|
stop: () => {
|
|
@@ -1100,75 +1221,154 @@ const plugin = {
|
|
|
1100
1221
|
},
|
|
1101
1222
|
});
|
|
1102
1223
|
|
|
1103
|
-
api.registerTool(
|
|
1224
|
+
api.registerTool((ctx) => ({
|
|
1104
1225
|
name: "kichi_join",
|
|
1105
|
-
|
|
1226
|
+
label: "kichi_join",
|
|
1227
|
+
description:
|
|
1228
|
+
"Join Kichi world in the target environment with avatarId, the current bot name, a short bio, and personality tags. For test, pass host.",
|
|
1106
1229
|
parameters: {
|
|
1107
1230
|
type: "object",
|
|
1108
1231
|
properties: {
|
|
1109
1232
|
avatarId: { type: "string", description: "Avatar ID to join Kichi world" },
|
|
1233
|
+
environment: {
|
|
1234
|
+
type: "string",
|
|
1235
|
+
enum: VALID_ENVIRONMENTS,
|
|
1236
|
+
description:
|
|
1237
|
+
"Target environment. kichi_join switches to this environment before joining.",
|
|
1238
|
+
},
|
|
1239
|
+
host: {
|
|
1240
|
+
type: "string",
|
|
1241
|
+
description: "Test host, required when environment is test and ignored otherwise",
|
|
1242
|
+
},
|
|
1110
1243
|
botName: {
|
|
1111
1244
|
type: "string",
|
|
1112
1245
|
description: "Current bot name to include in the join message",
|
|
1113
1246
|
},
|
|
1114
1247
|
bio: {
|
|
1115
1248
|
type: "string",
|
|
1116
|
-
description: "Short bio covering
|
|
1249
|
+
description: "Short bio extracted from SOUL.md, covering persona and idle plan goals if present",
|
|
1117
1250
|
},
|
|
1118
1251
|
tags: {
|
|
1119
1252
|
type: "array",
|
|
1120
1253
|
description: "Optional list of OpenClaw self-perceived personality tags",
|
|
1121
1254
|
items: { type: "string" },
|
|
1122
1255
|
},
|
|
1256
|
+
source: {
|
|
1257
|
+
type: "string",
|
|
1258
|
+
description: "Optional join source identifier. Defaults to Kichi World join-source.json, then openclaw.",
|
|
1259
|
+
},
|
|
1123
1260
|
},
|
|
1124
|
-
required: ["botName", "bio"],
|
|
1261
|
+
required: ["environment", "avatarId", "botName", "bio"],
|
|
1125
1262
|
},
|
|
1126
1263
|
execute: async (_toolCallId, params) => {
|
|
1127
|
-
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
(params as { tags?: unknown } | null)?.tags,
|
|
1132
|
-
);
|
|
1133
|
-
if (!avatarId) {
|
|
1134
|
-
avatarId = service.readSavedAvatarId() ?? undefined;
|
|
1264
|
+
const locator = resolveToolLocator(ctx);
|
|
1265
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1266
|
+
if (!agentId) {
|
|
1267
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1135
1268
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1269
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1270
|
+
const p = params as {
|
|
1271
|
+
avatarId?: string;
|
|
1272
|
+
environment?: unknown;
|
|
1273
|
+
host?: unknown;
|
|
1274
|
+
botName?: string;
|
|
1275
|
+
bio?: string;
|
|
1276
|
+
source?: unknown;
|
|
1277
|
+
tags?: unknown;
|
|
1278
|
+
} | null;
|
|
1279
|
+
const target = resolveJoinEnvironmentHost({
|
|
1280
|
+
environment: p?.environment,
|
|
1281
|
+
host: p?.host,
|
|
1282
|
+
});
|
|
1283
|
+
if (target.error) {
|
|
1284
|
+
return jsonResult({ success: false, error: target.error });
|
|
1138
1285
|
}
|
|
1286
|
+
const currentStatus = service.getConnectionStatus();
|
|
1287
|
+
let avatarId = p?.avatarId;
|
|
1288
|
+
if (!avatarId && currentStatus.host === target.host) {
|
|
1289
|
+
avatarId = service.readSavedAvatarId() ?? undefined;
|
|
1290
|
+
}
|
|
1291
|
+
const botName = p?.botName?.trim();
|
|
1292
|
+
const bio = p?.bio?.trim();
|
|
1293
|
+
const rawSource = p?.source;
|
|
1294
|
+
const { tags, error: tagsError } = normalizeJoinTags(
|
|
1295
|
+
p?.tags,
|
|
1296
|
+
);
|
|
1139
1297
|
if (!botName) {
|
|
1140
|
-
return { success: false, error: "No botName" };
|
|
1298
|
+
return jsonResult({ success: false, error: "No botName" });
|
|
1141
1299
|
}
|
|
1142
1300
|
if (!bio) {
|
|
1143
|
-
return { success: false, error: "No bio" };
|
|
1301
|
+
return jsonResult({ success: false, error: "No bio" });
|
|
1302
|
+
}
|
|
1303
|
+
let source: string | null | undefined;
|
|
1304
|
+
try {
|
|
1305
|
+
source = rawSource === undefined
|
|
1306
|
+
? service.readConfiguredJoinSource() ?? "openclaw"
|
|
1307
|
+
: trimOptionalString(rawSource);
|
|
1308
|
+
} catch (err) {
|
|
1309
|
+
return jsonResult({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
1310
|
+
}
|
|
1311
|
+
if (!source) {
|
|
1312
|
+
return jsonResult({ success: false, error: "source must be a non-empty string" });
|
|
1144
1313
|
}
|
|
1145
1314
|
if (tagsError) {
|
|
1146
|
-
return { success: false, error: tagsError };
|
|
1315
|
+
return jsonResult({ success: false, error: tagsError });
|
|
1316
|
+
}
|
|
1317
|
+
let leaveStatus;
|
|
1318
|
+
const shouldLeaveCurrentConnection = currentStatus.connected && currentStatus.hasAuthKey && (
|
|
1319
|
+
(!!currentStatus.host && currentStatus.host !== target.host) ||
|
|
1320
|
+
(currentStatus.host === target.host && !!currentStatus.avatarId && !!avatarId && currentStatus.avatarId !== avatarId)
|
|
1321
|
+
);
|
|
1322
|
+
if (shouldLeaveCurrentConnection) {
|
|
1323
|
+
try {
|
|
1324
|
+
leaveStatus = await service.leave();
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
leaveStatus = {
|
|
1327
|
+
success: false,
|
|
1328
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
let switchStatus;
|
|
1333
|
+
if (target.environment && target.host && service.getCurrentHost() !== target.host) {
|
|
1334
|
+
switchStatus = await service.switchHost(target.host, target.environment);
|
|
1335
|
+
}
|
|
1336
|
+
if (!avatarId) {
|
|
1337
|
+
avatarId = service.readSavedAvatarId() ?? undefined;
|
|
1338
|
+
}
|
|
1339
|
+
if (!avatarId) {
|
|
1340
|
+
return jsonResult({ success: false, error: "No avatarId" });
|
|
1147
1341
|
}
|
|
1148
|
-
const result = await service.join(avatarId, botName, bio, tags ?? []);
|
|
1342
|
+
const result = await service.join(avatarId, botName, bio, tags ?? [], source);
|
|
1149
1343
|
if (result.success) {
|
|
1150
|
-
return {
|
|
1344
|
+
return jsonResult({
|
|
1345
|
+
success: true,
|
|
1346
|
+
authKey: result.authKey,
|
|
1347
|
+
...(target.environment ? { environment: target.environment } : {}),
|
|
1348
|
+
...(target.host ? { host: target.host } : {}),
|
|
1349
|
+
...(switchStatus ? { switchStatus } : {}),
|
|
1350
|
+
...(leaveStatus ? { leaveStatus } : {}),
|
|
1351
|
+
});
|
|
1151
1352
|
}
|
|
1152
|
-
|
|
1353
|
+
const failure = result as { success: false; error: string; errorCode?: string; errorMessage?: string };
|
|
1354
|
+
return jsonResult({
|
|
1153
1355
|
success: false,
|
|
1154
|
-
error:
|
|
1155
|
-
...(
|
|
1156
|
-
...(
|
|
1157
|
-
|
|
1356
|
+
error: failure.error,
|
|
1357
|
+
...(target.environment ? { environment: target.environment } : {}),
|
|
1358
|
+
...(target.host ? { host: target.host } : {}),
|
|
1359
|
+
...(switchStatus ? { switchStatus } : {}),
|
|
1360
|
+
...(leaveStatus ? { leaveStatus } : {}),
|
|
1361
|
+
...(failure.errorCode ? { errorCode: failure.errorCode } : {}),
|
|
1362
|
+
...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
|
|
1363
|
+
});
|
|
1158
1364
|
},
|
|
1159
|
-
}))
|
|
1365
|
+
}), { name: "kichi_join" });
|
|
1160
1366
|
|
|
1161
|
-
api.registerTool((ctx
|
|
1162
|
-
const locator = resolveToolLocator(ctx);
|
|
1163
|
-
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1164
|
-
if (!agentId) {
|
|
1165
|
-
throw new Error("Failed to resolve agent-scoped Kichi runtime");
|
|
1166
|
-
}
|
|
1167
|
-
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1168
|
-
return ({
|
|
1367
|
+
api.registerTool((ctx) => ({
|
|
1169
1368
|
name: "kichi_switch_host",
|
|
1369
|
+
label: "kichi_switch_host",
|
|
1170
1370
|
description:
|
|
1171
|
-
"Switch Kichi runtime environment and reconnect immediately without restarting the gateway.
|
|
1371
|
+
"Switch Kichi runtime environment and reconnect immediately without restarting the gateway. For steam/steam-playtest the host is resolved automatically. For test, pass the host explicitly.",
|
|
1172
1372
|
parameters: {
|
|
1173
1373
|
type: "object",
|
|
1174
1374
|
properties: {
|
|
@@ -1177,79 +1377,125 @@ const plugin = {
|
|
|
1177
1377
|
enum: VALID_ENVIRONMENTS,
|
|
1178
1378
|
description: "Target environment: steam, steam-playtest, or test",
|
|
1179
1379
|
},
|
|
1380
|
+
host: {
|
|
1381
|
+
type: "string",
|
|
1382
|
+
description: "Test host (required for test environment, ignored otherwise)",
|
|
1383
|
+
},
|
|
1180
1384
|
},
|
|
1181
1385
|
required: ["environment"],
|
|
1182
1386
|
},
|
|
1183
1387
|
execute: async (_toolCallId, params) => {
|
|
1184
|
-
const
|
|
1388
|
+
const locator = resolveToolLocator(ctx);
|
|
1389
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1390
|
+
if (!agentId) {
|
|
1391
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1392
|
+
}
|
|
1393
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1394
|
+
const p = params as { environment?: unknown; host?: unknown } | null;
|
|
1395
|
+
const environment = p?.environment;
|
|
1185
1396
|
if (!isKichiEnvironment(environment)) {
|
|
1186
|
-
return { success: false, error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` };
|
|
1397
|
+
return jsonResult({ success: false, error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` });
|
|
1187
1398
|
}
|
|
1188
1399
|
|
|
1189
|
-
|
|
1190
|
-
if (
|
|
1191
|
-
|
|
1400
|
+
let targetHost: string;
|
|
1401
|
+
if (environment === "test") {
|
|
1402
|
+
const testHost = typeof p?.host === "string" ? p.host.trim() : "";
|
|
1403
|
+
if (!testHost) {
|
|
1404
|
+
return jsonResult({ success: false, error: "host is required for the test environment" });
|
|
1405
|
+
}
|
|
1406
|
+
targetHost = testHost;
|
|
1407
|
+
} else {
|
|
1408
|
+
const resolved = resolveEnvironmentHost(environment);
|
|
1409
|
+
if (resolved.error) {
|
|
1410
|
+
return jsonResult({ success: false, error: resolved.error });
|
|
1411
|
+
}
|
|
1412
|
+
targetHost = resolved.host!;
|
|
1192
1413
|
}
|
|
1193
1414
|
|
|
1194
|
-
const status = await service.switchHost(
|
|
1195
|
-
return {
|
|
1415
|
+
const status = await service.switchHost(targetHost, environment);
|
|
1416
|
+
return jsonResult({
|
|
1196
1417
|
success: true,
|
|
1197
1418
|
environment,
|
|
1198
|
-
host:
|
|
1419
|
+
host: targetHost,
|
|
1199
1420
|
status,
|
|
1200
|
-
};
|
|
1421
|
+
});
|
|
1201
1422
|
},
|
|
1202
|
-
|
|
1203
|
-
});
|
|
1423
|
+
}), { name: "kichi_switch_host" });
|
|
1204
1424
|
|
|
1205
|
-
api.registerTool(
|
|
1425
|
+
api.registerTool((ctx) => ({
|
|
1206
1426
|
name: "kichi_rejoin",
|
|
1427
|
+
label: "kichi_rejoin",
|
|
1207
1428
|
description:
|
|
1208
1429
|
"Request an immediate rejoin attempt with saved avatarId/authKey. Rejoin is also sent automatically after reconnect.",
|
|
1209
1430
|
parameters: { type: "object", properties: {} },
|
|
1210
|
-
execute: async () => {
|
|
1431
|
+
execute: async (_toolCallId, _params) => {
|
|
1432
|
+
const locator = resolveToolLocator(ctx);
|
|
1433
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1434
|
+
if (!agentId) {
|
|
1435
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1436
|
+
}
|
|
1437
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1211
1438
|
const result = service.requestRejoin();
|
|
1212
|
-
return {
|
|
1439
|
+
return jsonResult({
|
|
1213
1440
|
success: result.accepted,
|
|
1214
1441
|
...result,
|
|
1215
1442
|
status: service.getConnectionStatus(),
|
|
1216
|
-
};
|
|
1443
|
+
});
|
|
1217
1444
|
},
|
|
1218
|
-
}))
|
|
1445
|
+
}), { name: "kichi_rejoin" });
|
|
1219
1446
|
|
|
1220
|
-
api.registerTool(
|
|
1447
|
+
api.registerTool((ctx) => ({
|
|
1221
1448
|
name: "kichi_leave",
|
|
1449
|
+
label: "kichi_leave",
|
|
1222
1450
|
description: "Leave Kichi world",
|
|
1223
1451
|
parameters: { type: "object", properties: {} },
|
|
1224
|
-
execute: async () => {
|
|
1452
|
+
execute: async (_toolCallId, _params) => {
|
|
1453
|
+
const locator = resolveToolLocator(ctx);
|
|
1454
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1455
|
+
if (!agentId) {
|
|
1456
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1457
|
+
}
|
|
1458
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1225
1459
|
const result = await service.leave();
|
|
1226
1460
|
if (result.success) {
|
|
1227
|
-
return { success: true };
|
|
1461
|
+
return jsonResult({ success: true });
|
|
1228
1462
|
}
|
|
1229
|
-
|
|
1463
|
+
const failure = result as { success: false; error: string; errorCode?: string; errorMessage?: string };
|
|
1464
|
+
return jsonResult({
|
|
1230
1465
|
success: false,
|
|
1231
|
-
error:
|
|
1232
|
-
...(
|
|
1233
|
-
...(
|
|
1234
|
-
};
|
|
1466
|
+
error: failure.error,
|
|
1467
|
+
...(failure.errorCode ? { errorCode: failure.errorCode } : {}),
|
|
1468
|
+
...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
|
|
1469
|
+
});
|
|
1235
1470
|
},
|
|
1236
|
-
}))
|
|
1471
|
+
}), { name: "kichi_leave" });
|
|
1237
1472
|
|
|
1238
|
-
api.registerTool(
|
|
1473
|
+
api.registerTool((ctx) => ({
|
|
1239
1474
|
name: "kichi_connection_status",
|
|
1475
|
+
label: "kichi_connection_status",
|
|
1240
1476
|
description: "Check WebSocket connection status and identity readiness only. Does NOT return room info, avatar state, or personnel — use kichi_query_status for that.",
|
|
1241
1477
|
parameters: { type: "object", properties: {} },
|
|
1242
|
-
execute: async () => {
|
|
1243
|
-
|
|
1478
|
+
execute: async (_toolCallId, _params) => {
|
|
1479
|
+
const locator = resolveToolLocator(ctx);
|
|
1480
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1481
|
+
if (!agentId) {
|
|
1482
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1483
|
+
}
|
|
1484
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1485
|
+
return jsonResult({
|
|
1244
1486
|
success: true,
|
|
1245
1487
|
status: service.getConnectionStatus(),
|
|
1246
|
-
};
|
|
1488
|
+
});
|
|
1247
1489
|
},
|
|
1248
|
-
}))
|
|
1490
|
+
}), { name: "kichi_connection_status" });
|
|
1249
1491
|
|
|
1250
|
-
api.registerTool(
|
|
1492
|
+
api.registerTool((ctx) => {
|
|
1493
|
+
const locator = resolveToolLocator(ctx);
|
|
1494
|
+
const existingService = runtimeManager.getRuntime(locator);
|
|
1495
|
+
return ({
|
|
1251
1496
|
name: "kichi_action",
|
|
1252
|
-
|
|
1497
|
+
label: "kichi_action",
|
|
1498
|
+
description: buildKichiActionDescription(existingService ?? undefined),
|
|
1253
1499
|
parameters: {
|
|
1254
1500
|
type: "object",
|
|
1255
1501
|
properties: {
|
|
@@ -1259,6 +1505,11 @@ const plugin = {
|
|
|
1259
1505
|
description: "Action name for the selected pose (for example Sit Nicely, Typing with Keyboard, Reading, High Five, or Meditate)",
|
|
1260
1506
|
},
|
|
1261
1507
|
bubble: { type: "string", description: "Optional bubble text to display (max 5 words)" },
|
|
1508
|
+
avatarStatus: {
|
|
1509
|
+
type: "string",
|
|
1510
|
+
description: "Current avatar status: Idle, Busy, Activities, or Break.",
|
|
1511
|
+
enum: [...AVATAR_STATUSES],
|
|
1512
|
+
},
|
|
1262
1513
|
log: {
|
|
1263
1514
|
type: "string",
|
|
1264
1515
|
description:
|
|
@@ -1269,39 +1520,56 @@ const plugin = {
|
|
|
1269
1520
|
description:
|
|
1270
1521
|
"Set true ONLY when the user explicitly requests a pose or action. Omit during routine sync steps.",
|
|
1271
1522
|
},
|
|
1523
|
+
propId: {
|
|
1524
|
+
type: "string",
|
|
1525
|
+
description:
|
|
1526
|
+
"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.",
|
|
1527
|
+
},
|
|
1272
1528
|
},
|
|
1273
|
-
required: ["poseType", "action"],
|
|
1529
|
+
required: ["poseType", "action", "avatarStatus"],
|
|
1274
1530
|
},
|
|
1275
1531
|
execute: async (_toolCallId, params) => {
|
|
1276
|
-
const
|
|
1532
|
+
const locator = resolveToolLocator(ctx);
|
|
1533
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1534
|
+
if (!agentId) {
|
|
1535
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1536
|
+
}
|
|
1537
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1538
|
+
const { poseType, action, bubble, avatarStatus, log, verify, propId } = (params || {}) as {
|
|
1277
1539
|
poseType?: string;
|
|
1278
1540
|
action?: string;
|
|
1279
1541
|
bubble?: string;
|
|
1542
|
+
avatarStatus?: unknown;
|
|
1280
1543
|
log?: string;
|
|
1281
1544
|
verify?: boolean;
|
|
1545
|
+
propId?: string;
|
|
1282
1546
|
};
|
|
1283
1547
|
if (!poseType || !action) {
|
|
1284
|
-
return { success: false, error: "poseType and action parameters are required" };
|
|
1548
|
+
return jsonResult({ success: false, error: "poseType and action parameters are required" });
|
|
1285
1549
|
}
|
|
1286
1550
|
if (!["stand", "sit", "lay", "floor"].includes(poseType)) {
|
|
1287
|
-
return {
|
|
1551
|
+
return jsonResult({
|
|
1288
1552
|
success: false,
|
|
1289
1553
|
error: `Invalid poseType: ${poseType}. Must be stand, sit, lay, or floor`,
|
|
1290
|
-
};
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
const normalizedAvatarStatus = normalizeAvatarStatus(avatarStatus, "avatarStatus");
|
|
1557
|
+
if (normalizedAvatarStatus.error || normalizedAvatarStatus.avatarStatus === undefined) {
|
|
1558
|
+
return jsonResult({ success: false, error: normalizedAvatarStatus.error ?? "avatarStatus is invalid" });
|
|
1291
1559
|
}
|
|
1292
1560
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1293
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1561
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1294
1562
|
}
|
|
1295
1563
|
|
|
1296
1564
|
const normalizedPoseType = poseType as PoseType;
|
|
1297
1565
|
const poseActions = loadStaticConfig().actions[normalizedPoseType];
|
|
1298
1566
|
const matched = poseActions.find((entry) => entry.name.toLowerCase() === action.toLowerCase());
|
|
1299
1567
|
if (!matched) {
|
|
1300
|
-
return {
|
|
1568
|
+
return jsonResult({
|
|
1301
1569
|
success: false,
|
|
1302
1570
|
error: `Unknown action "${action}" for poseType "${poseType}"`,
|
|
1303
1571
|
available: poseActions.map((entry) => entry.name),
|
|
1304
|
-
};
|
|
1572
|
+
});
|
|
1305
1573
|
}
|
|
1306
1574
|
|
|
1307
1575
|
const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : matched.name;
|
|
@@ -1311,15 +1579,21 @@ const plugin = {
|
|
|
1311
1579
|
if (verify) {
|
|
1312
1580
|
try {
|
|
1313
1581
|
const ack = await service.sendStatusVerified(
|
|
1314
|
-
normalizedPoseType,
|
|
1582
|
+
normalizedPoseType,
|
|
1583
|
+
matched.name,
|
|
1584
|
+
bubbleText,
|
|
1585
|
+
logText,
|
|
1586
|
+
playback,
|
|
1587
|
+
normalizedAvatarStatus.avatarStatus,
|
|
1588
|
+
propId,
|
|
1315
1589
|
);
|
|
1316
1590
|
if (ack.warning) {
|
|
1317
|
-
return {
|
|
1591
|
+
return jsonResult({
|
|
1318
1592
|
success: true,
|
|
1319
1593
|
requested: { poseType: normalizedPoseType, action: matched.name },
|
|
1320
1594
|
actual: { poseType: ack.poseType, action: ack.action },
|
|
1321
1595
|
warning: ack.warning,
|
|
1322
|
-
};
|
|
1596
|
+
});
|
|
1323
1597
|
}
|
|
1324
1598
|
} catch {
|
|
1325
1599
|
// Server not updated or timeout — fall through to normal success
|
|
@@ -1330,21 +1604,90 @@ const plugin = {
|
|
|
1330
1604
|
action: matched.name,
|
|
1331
1605
|
bubble: bubbleText,
|
|
1332
1606
|
log: logText,
|
|
1607
|
+
avatarStatus: normalizedAvatarStatus.avatarStatus,
|
|
1608
|
+
propId,
|
|
1333
1609
|
});
|
|
1334
1610
|
}
|
|
1335
1611
|
|
|
1336
|
-
return {
|
|
1612
|
+
return jsonResult({
|
|
1337
1613
|
success: true,
|
|
1338
1614
|
poseType: normalizedPoseType,
|
|
1339
1615
|
action: matched.name,
|
|
1340
1616
|
bubble: bubbleText,
|
|
1341
1617
|
log: logText,
|
|
1618
|
+
avatarStatus: normalizedAvatarStatus.avatarStatus,
|
|
1342
1619
|
playback,
|
|
1620
|
+
});
|
|
1621
|
+
},
|
|
1622
|
+
})}, { name: "kichi_action" });
|
|
1623
|
+
|
|
1624
|
+
api.registerTool((ctx) => ({
|
|
1625
|
+
name: "kichi_glance",
|
|
1626
|
+
label: "kichi_glance",
|
|
1627
|
+
description:
|
|
1628
|
+
"Ask the Kichi avatar to briefly look at the camera. Use only for direct player chat requests such as \"look at me\" or \"look at the camera\". Do not use for heartbeat, idle planning, bot-to-bot messages, lifecycle hooks, or routine work/status sync.",
|
|
1629
|
+
parameters: {
|
|
1630
|
+
type: "object",
|
|
1631
|
+
properties: {
|
|
1632
|
+
requestId: {
|
|
1633
|
+
type: "string",
|
|
1634
|
+
description: "Optional client request ID for tracing. The websocket ack returns this ID.",
|
|
1635
|
+
},
|
|
1636
|
+
target: {
|
|
1637
|
+
type: "string",
|
|
1638
|
+
enum: ["camera"],
|
|
1639
|
+
description: "Glance target. The only supported target is camera.",
|
|
1640
|
+
},
|
|
1641
|
+
duration: {
|
|
1642
|
+
type: "number",
|
|
1643
|
+
description: "Optional glance duration in seconds. Defaults to 1.8.",
|
|
1644
|
+
},
|
|
1645
|
+
},
|
|
1646
|
+
},
|
|
1647
|
+
execute: async (_toolCallId, params) => {
|
|
1648
|
+
const locator = resolveToolLocator(ctx);
|
|
1649
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1650
|
+
if (!agentId) {
|
|
1651
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1652
|
+
}
|
|
1653
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1654
|
+
const { requestId, target, duration } = (params || {}) as {
|
|
1655
|
+
requestId?: unknown;
|
|
1656
|
+
target?: unknown;
|
|
1657
|
+
duration?: unknown;
|
|
1343
1658
|
};
|
|
1659
|
+
|
|
1660
|
+
if (requestId !== undefined && typeof requestId !== "string") {
|
|
1661
|
+
return jsonResult({ success: false, error: "requestId must be a string when provided" });
|
|
1662
|
+
}
|
|
1663
|
+
const normalizedTarget = target === undefined ? "camera" : target;
|
|
1664
|
+
if (normalizedTarget !== "camera") {
|
|
1665
|
+
return jsonResult({ success: false, error: "target must be camera" });
|
|
1666
|
+
}
|
|
1667
|
+
const normalizedDuration = duration === undefined ? DEFAULT_GLANCE_DURATION_SECONDS : duration;
|
|
1668
|
+
if (typeof normalizedDuration !== "number" || !Number.isFinite(normalizedDuration) || normalizedDuration <= 0) {
|
|
1669
|
+
return jsonResult({ success: false, error: "duration must be a positive finite number" });
|
|
1670
|
+
}
|
|
1671
|
+
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1672
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
try {
|
|
1676
|
+
const ack = await service.sendGlance(
|
|
1677
|
+
"camera",
|
|
1678
|
+
normalizedDuration,
|
|
1679
|
+
typeof requestId === "string" ? requestId : undefined,
|
|
1680
|
+
);
|
|
1681
|
+
return jsonResult({ success: true, ...ack });
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
return jsonResult({ success: false, error: `Failed to send glance: ${error}` });
|
|
1684
|
+
}
|
|
1344
1685
|
},
|
|
1345
|
-
}))
|
|
1346
|
-
|
|
1686
|
+
}), { name: "kichi_glance" });
|
|
1687
|
+
|
|
1688
|
+
api.registerTool((ctx) => ({
|
|
1347
1689
|
name: "kichi_idle_plan",
|
|
1690
|
+
label: "kichi_idle_plan",
|
|
1348
1691
|
description: buildKichiIdlePlanDescription(),
|
|
1349
1692
|
parameters: {
|
|
1350
1693
|
type: "object",
|
|
@@ -1380,6 +1723,11 @@ const plugin = {
|
|
|
1380
1723
|
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.",
|
|
1381
1724
|
enum: [...IDLE_PLAN_POMODORO_PHASES],
|
|
1382
1725
|
},
|
|
1726
|
+
avatarStatus: {
|
|
1727
|
+
type: "string",
|
|
1728
|
+
description: "Avatar status for this stage: Idle, Busy, Activities, or Break.",
|
|
1729
|
+
enum: [...AVATAR_STATUSES],
|
|
1730
|
+
},
|
|
1383
1731
|
durationSeconds: {
|
|
1384
1732
|
type: "number",
|
|
1385
1733
|
description: "Required duration in seconds for this stage.",
|
|
@@ -1410,24 +1758,34 @@ const plugin = {
|
|
|
1410
1758
|
type: "string",
|
|
1411
1759
|
description: "Optional log content for this action. Use the same language as the current conversation.",
|
|
1412
1760
|
},
|
|
1761
|
+
propId: {
|
|
1762
|
+
type: "string",
|
|
1763
|
+
description: "Optional poseable prop ID from RoomContext.PoseableProps. When specified, the avatar is seated at this prop.",
|
|
1764
|
+
},
|
|
1413
1765
|
},
|
|
1414
1766
|
required: ["poseType", "action", "durationSeconds", "bubble"],
|
|
1415
1767
|
},
|
|
1416
1768
|
},
|
|
1417
1769
|
},
|
|
1418
|
-
required: ["name", "purpose", "pomodoroPhase", "durationSeconds", "actions"],
|
|
1770
|
+
required: ["name", "purpose", "pomodoroPhase", "avatarStatus", "durationSeconds", "actions"],
|
|
1419
1771
|
},
|
|
1420
1772
|
},
|
|
1421
1773
|
},
|
|
1422
1774
|
required: ["heartbeatIntervalSeconds", "goal", "stages"],
|
|
1423
1775
|
},
|
|
1424
1776
|
execute: async (_toolCallId, params) => {
|
|
1777
|
+
const locator = resolveToolLocator(ctx);
|
|
1778
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1779
|
+
if (!agentId) {
|
|
1780
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1781
|
+
}
|
|
1782
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1425
1783
|
const { idlePlan, error } = normalizeIdlePlan(params);
|
|
1426
1784
|
if (!idlePlan) {
|
|
1427
|
-
return { success: false, error: error ?? "Invalid idle plan payload" };
|
|
1785
|
+
return jsonResult({ success: false, error: error ?? "Invalid idle plan payload" });
|
|
1428
1786
|
}
|
|
1429
1787
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1430
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1788
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1431
1789
|
}
|
|
1432
1790
|
const sent = service.sendIdlePlan({
|
|
1433
1791
|
...(idlePlan.requestId ? { requestId: idlePlan.requestId } : {}),
|
|
@@ -1436,20 +1794,21 @@ const plugin = {
|
|
|
1436
1794
|
stages: idlePlan.stages,
|
|
1437
1795
|
});
|
|
1438
1796
|
if (!sent) {
|
|
1439
|
-
return { success: false, error: "Failed to send idle plan payload" };
|
|
1797
|
+
return jsonResult({ success: false, error: "Failed to send idle plan payload" });
|
|
1440
1798
|
}
|
|
1441
|
-
return {
|
|
1799
|
+
return jsonResult({
|
|
1442
1800
|
success: true,
|
|
1443
1801
|
...(idlePlan.requestId ? { requestId: idlePlan.requestId } : {}),
|
|
1444
1802
|
heartbeatIntervalSeconds: idlePlan.heartbeatIntervalSeconds,
|
|
1445
1803
|
totalDurationSeconds: idlePlan.totalDurationSeconds,
|
|
1446
1804
|
goal: idlePlan.goal,
|
|
1447
1805
|
stages: idlePlan.stages,
|
|
1448
|
-
};
|
|
1806
|
+
});
|
|
1449
1807
|
},
|
|
1450
|
-
}))
|
|
1451
|
-
api.registerTool(
|
|
1808
|
+
}), { name: "kichi_idle_plan" });
|
|
1809
|
+
api.registerTool((ctx) => ({
|
|
1452
1810
|
name: "kichi_clock",
|
|
1811
|
+
label: "kichi_clock",
|
|
1453
1812
|
description:
|
|
1454
1813
|
"Send clock commands to Kichi world. Supported actions are set and stop.",
|
|
1455
1814
|
parameters: {
|
|
@@ -1517,6 +1876,12 @@ const plugin = {
|
|
|
1517
1876
|
required: ["action"],
|
|
1518
1877
|
},
|
|
1519
1878
|
execute: async (_toolCallId, params) => {
|
|
1879
|
+
const locator = resolveToolLocator(ctx);
|
|
1880
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1881
|
+
if (!agentId) {
|
|
1882
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1883
|
+
}
|
|
1884
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1520
1885
|
const { action, requestId, clock } = (params || {}) as {
|
|
1521
1886
|
action?: unknown;
|
|
1522
1887
|
requestId?: unknown;
|
|
@@ -1524,46 +1889,47 @@ const plugin = {
|
|
|
1524
1889
|
};
|
|
1525
1890
|
|
|
1526
1891
|
if (!isClockAction(action)) {
|
|
1527
|
-
return {
|
|
1892
|
+
return jsonResult({
|
|
1528
1893
|
success: false,
|
|
1529
1894
|
error: "action must be one of: set, stop",
|
|
1530
|
-
};
|
|
1895
|
+
});
|
|
1531
1896
|
}
|
|
1532
1897
|
if (requestId !== undefined && typeof requestId !== "string") {
|
|
1533
|
-
return { success: false, error: "requestId must be a string when provided" };
|
|
1898
|
+
return jsonResult({ success: false, error: "requestId must be a string when provided" });
|
|
1534
1899
|
}
|
|
1535
1900
|
const normalizedRequestId = typeof requestId === "string" ? requestId : undefined;
|
|
1536
1901
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1537
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1902
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1538
1903
|
}
|
|
1539
1904
|
|
|
1540
1905
|
let normalizedClock: ClockConfig | undefined;
|
|
1541
1906
|
if (action === "set") {
|
|
1542
1907
|
const { clock: nextClock, error } = normalizeClockConfig(clock);
|
|
1543
1908
|
if (!nextClock) {
|
|
1544
|
-
return { success: false, error: error ?? "Invalid clock payload" };
|
|
1909
|
+
return jsonResult({ success: false, error: error ?? "Invalid clock payload" });
|
|
1545
1910
|
}
|
|
1546
1911
|
normalizedClock = nextClock;
|
|
1547
1912
|
}
|
|
1548
1913
|
|
|
1549
1914
|
const sent = service.sendClock(action, normalizedClock, normalizedRequestId);
|
|
1550
1915
|
if (!sent) {
|
|
1551
|
-
return { success: false, error: "Failed to send clock payload" };
|
|
1916
|
+
return jsonResult({ success: false, error: "Failed to send clock payload" });
|
|
1552
1917
|
}
|
|
1553
1918
|
|
|
1554
|
-
return {
|
|
1919
|
+
return jsonResult({
|
|
1555
1920
|
success: true,
|
|
1556
1921
|
action,
|
|
1557
1922
|
requestId: normalizedRequestId,
|
|
1558
1923
|
...(normalizedClock ? { clock: normalizedClock } : {}),
|
|
1559
|
-
};
|
|
1924
|
+
});
|
|
1560
1925
|
},
|
|
1561
|
-
}))
|
|
1926
|
+
}), { name: "kichi_clock" });
|
|
1562
1927
|
|
|
1563
|
-
api.registerTool(
|
|
1928
|
+
api.registerTool((ctx) => ({
|
|
1564
1929
|
name: "kichi_query_status",
|
|
1930
|
+
label: "kichi_query_status",
|
|
1565
1931
|
description:
|
|
1566
|
-
"Query Kichi room and avatar status — includes room personnel, notes, ownerState, idlePlan, weather/time, timer snapshot, daily note quota,
|
|
1932
|
+
"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, Description, 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.",
|
|
1567
1933
|
parameters: {
|
|
1568
1934
|
type: "object",
|
|
1569
1935
|
properties: {
|
|
@@ -1574,30 +1940,37 @@ const plugin = {
|
|
|
1574
1940
|
},
|
|
1575
1941
|
},
|
|
1576
1942
|
execute: async (_toolCallId, params) => {
|
|
1943
|
+
const locator = resolveToolLocator(ctx);
|
|
1944
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1945
|
+
if (!agentId) {
|
|
1946
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1947
|
+
}
|
|
1948
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1577
1949
|
const requestId = (params as { requestId?: unknown } | null)?.requestId;
|
|
1578
1950
|
if (requestId !== undefined && typeof requestId !== "string") {
|
|
1579
|
-
return { success: false, error: "requestId must be a string when provided" };
|
|
1951
|
+
return jsonResult({ success: false, error: "requestId must be a string when provided" });
|
|
1580
1952
|
}
|
|
1581
1953
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1582
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1954
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1583
1955
|
}
|
|
1584
1956
|
|
|
1585
1957
|
try {
|
|
1586
1958
|
const result = await service.queryStatus(
|
|
1587
1959
|
typeof requestId === "string" ? requestId : undefined,
|
|
1588
1960
|
);
|
|
1589
|
-
return result;
|
|
1961
|
+
return jsonResult(result);
|
|
1590
1962
|
} catch (error) {
|
|
1591
|
-
return {
|
|
1963
|
+
return jsonResult({
|
|
1592
1964
|
success: false,
|
|
1593
1965
|
error: `Failed to query status: ${error}`,
|
|
1594
|
-
};
|
|
1966
|
+
});
|
|
1595
1967
|
}
|
|
1596
1968
|
},
|
|
1597
|
-
}))
|
|
1969
|
+
}), { name: "kichi_query_status" });
|
|
1598
1970
|
|
|
1599
|
-
api.registerTool(
|
|
1971
|
+
api.registerTool((ctx) => ({
|
|
1600
1972
|
name: "kichi_music_album_create",
|
|
1973
|
+
label: "kichi_music_album_create",
|
|
1601
1974
|
description: buildMusicAlbumToolDescription(),
|
|
1602
1975
|
parameters: {
|
|
1603
1976
|
type: "object",
|
|
@@ -1622,6 +1995,12 @@ const plugin = {
|
|
|
1622
1995
|
required: ["albumTitle", "musicTitles"],
|
|
1623
1996
|
},
|
|
1624
1997
|
execute: async (_toolCallId, params) => {
|
|
1998
|
+
const locator = resolveToolLocator(ctx);
|
|
1999
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
2000
|
+
if (!agentId) {
|
|
2001
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
2002
|
+
}
|
|
2003
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1625
2004
|
const {
|
|
1626
2005
|
requestId,
|
|
1627
2006
|
albumTitle,
|
|
@@ -1633,33 +2012,33 @@ const plugin = {
|
|
|
1633
2012
|
};
|
|
1634
2013
|
|
|
1635
2014
|
if (requestId !== undefined && typeof requestId !== "string") {
|
|
1636
|
-
return { success: false, error: "requestId must be a string when provided" };
|
|
2015
|
+
return jsonResult({ success: false, error: "requestId must be a string when provided" });
|
|
1637
2016
|
}
|
|
1638
2017
|
if (typeof albumTitle !== "string" || !albumTitle.trim()) {
|
|
1639
|
-
return { success: false, error: "albumTitle is required" };
|
|
2018
|
+
return jsonResult({ success: false, error: "albumTitle is required" });
|
|
1640
2019
|
}
|
|
1641
2020
|
if (!Array.isArray(musicTitles)) {
|
|
1642
|
-
return { success: false, error: "musicTitles must be an array of track names" };
|
|
2021
|
+
return jsonResult({ success: false, error: "musicTitles must be an array of track names" });
|
|
1643
2022
|
}
|
|
1644
2023
|
|
|
1645
2024
|
const { titles: normalizedTitles, invalidTitles } = normalizeMusicTitles(musicTitles);
|
|
1646
2025
|
if (normalizedTitles.length === 0) {
|
|
1647
|
-
return {
|
|
2026
|
+
return jsonResult({
|
|
1648
2027
|
success: false,
|
|
1649
2028
|
error: "musicTitles must contain at least one valid track name from the static config bundled with the plugin package",
|
|
1650
2029
|
examples: getMusicTitleExamples(),
|
|
1651
|
-
};
|
|
2030
|
+
});
|
|
1652
2031
|
}
|
|
1653
2032
|
if (invalidTitles.length > 0) {
|
|
1654
|
-
return {
|
|
2033
|
+
return jsonResult({
|
|
1655
2034
|
success: false,
|
|
1656
2035
|
error: `Unknown musicTitles: ${invalidTitles.join(", ")}`,
|
|
1657
2036
|
hint: "Use exact track names from the static config bundled with the plugin package",
|
|
1658
2037
|
examples: getMusicTitleExamples(),
|
|
1659
|
-
};
|
|
2038
|
+
});
|
|
1660
2039
|
}
|
|
1661
2040
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1662
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
2041
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1663
2042
|
}
|
|
1664
2043
|
|
|
1665
2044
|
try {
|
|
@@ -1668,24 +2047,25 @@ const plugin = {
|
|
|
1668
2047
|
normalizedTitles,
|
|
1669
2048
|
typeof requestId === "string" ? requestId : undefined,
|
|
1670
2049
|
);
|
|
1671
|
-
return {
|
|
2050
|
+
return jsonResult({
|
|
1672
2051
|
success: true,
|
|
1673
2052
|
requestId: normalizedRequestId,
|
|
1674
2053
|
albumTitle: albumTitle.trim(),
|
|
1675
2054
|
musicTitles: normalizedTitles,
|
|
1676
2055
|
trackCount: normalizedTitles.length,
|
|
1677
|
-
};
|
|
2056
|
+
});
|
|
1678
2057
|
} catch (error) {
|
|
1679
|
-
return {
|
|
2058
|
+
return jsonResult({
|
|
1680
2059
|
success: false,
|
|
1681
2060
|
error: `Failed to create music album: ${error}`,
|
|
1682
|
-
};
|
|
2061
|
+
});
|
|
1683
2062
|
}
|
|
1684
2063
|
},
|
|
1685
|
-
}))
|
|
2064
|
+
}), { name: "kichi_music_album_create" });
|
|
1686
2065
|
|
|
1687
|
-
api.registerTool(
|
|
2066
|
+
api.registerTool((ctx) => ({
|
|
1688
2067
|
name: "kichi_noteboard_create",
|
|
2068
|
+
label: "kichi_noteboard_create",
|
|
1689
2069
|
description:
|
|
1690
2070
|
"Create a new note on a specific Kichi note board. Prefer querying first so you can avoid duplicate posts and respect rate limits.",
|
|
1691
2071
|
parameters: {
|
|
@@ -1703,40 +2083,127 @@ const plugin = {
|
|
|
1703
2083
|
required: ["propId", "data"],
|
|
1704
2084
|
},
|
|
1705
2085
|
execute: async (_toolCallId, params) => {
|
|
2086
|
+
const locator = resolveToolLocator(ctx);
|
|
2087
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
2088
|
+
if (!agentId) {
|
|
2089
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
2090
|
+
}
|
|
2091
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1706
2092
|
const { propId, data } = (params || {}) as {
|
|
1707
2093
|
propId?: unknown;
|
|
1708
2094
|
data?: unknown;
|
|
1709
2095
|
};
|
|
1710
2096
|
if (typeof propId !== "string" || !propId.trim()) {
|
|
1711
|
-
return { success: false, error: "propId is required" };
|
|
2097
|
+
return jsonResult({ success: false, error: "propId is required" });
|
|
1712
2098
|
}
|
|
1713
2099
|
if (typeof data !== "string" || !data.trim()) {
|
|
1714
|
-
return { success: false, error: "data is required" };
|
|
2100
|
+
return jsonResult({ success: false, error: "data is required" });
|
|
1715
2101
|
}
|
|
1716
2102
|
if (data.trim().length > MAX_NOTEBOARD_TEXT_LENGTH) {
|
|
1717
|
-
return {
|
|
2103
|
+
return jsonResult({
|
|
1718
2104
|
success: false,
|
|
1719
2105
|
error: `data must be ${MAX_NOTEBOARD_TEXT_LENGTH} characters or fewer`,
|
|
1720
|
-
};
|
|
2106
|
+
});
|
|
1721
2107
|
}
|
|
1722
2108
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1723
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
2109
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1724
2110
|
}
|
|
1725
2111
|
|
|
1726
2112
|
try {
|
|
1727
2113
|
service.createNotesBoardNote(propId.trim(), data.trim());
|
|
1728
|
-
return { success: true };
|
|
2114
|
+
return jsonResult({ success: true });
|
|
1729
2115
|
} catch (error) {
|
|
1730
|
-
return {
|
|
2116
|
+
return jsonResult({
|
|
1731
2117
|
success: false,
|
|
1732
2118
|
error: `Failed to create note: ${error}`,
|
|
1733
|
-
};
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
},
|
|
2122
|
+
}), { name: "kichi_noteboard_create" });
|
|
2123
|
+
|
|
2124
|
+
api.registerTool((ctx) => ({
|
|
2125
|
+
name: "kichi_bot_message",
|
|
2126
|
+
label: "kichi_bot_message",
|
|
2127
|
+
description:
|
|
2128
|
+
"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.",
|
|
2129
|
+
parameters: {
|
|
2130
|
+
type: "object",
|
|
2131
|
+
properties: {
|
|
2132
|
+
toAvatarId: {
|
|
2133
|
+
type: "string",
|
|
2134
|
+
description: "Target bot's avatarId (resolve via kichi_query_status if unknown). Use \"*\" only for broadcasting to all bots.",
|
|
2135
|
+
},
|
|
2136
|
+
depth: {
|
|
2137
|
+
type: "number",
|
|
2138
|
+
description: "Conversation depth counter. Increment from the received message's depth.",
|
|
2139
|
+
},
|
|
2140
|
+
bubble: {
|
|
2141
|
+
type: "string",
|
|
2142
|
+
description: "The message to send (2-5 words, visible to everyone). Must not repeat previous messages.",
|
|
2143
|
+
},
|
|
2144
|
+
poseType: {
|
|
2145
|
+
type: "string",
|
|
2146
|
+
enum: ["stand", "sit", "lay", "floor"],
|
|
2147
|
+
description: "Optional pose change when sending.",
|
|
2148
|
+
},
|
|
2149
|
+
action: {
|
|
2150
|
+
type: "string",
|
|
2151
|
+
description: "Optional action to perform when sending.",
|
|
2152
|
+
},
|
|
2153
|
+
log: {
|
|
2154
|
+
type: "string",
|
|
2155
|
+
description: "Optional activity log entry.",
|
|
2156
|
+
},
|
|
2157
|
+
},
|
|
2158
|
+
required: ["toAvatarId", "depth", "bubble"],
|
|
2159
|
+
},
|
|
2160
|
+
execute: async (_toolCallId, params) => {
|
|
2161
|
+
const locator = resolveToolLocator(ctx);
|
|
2162
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
2163
|
+
if (!agentId) {
|
|
2164
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
2165
|
+
}
|
|
2166
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
2167
|
+
const { toAvatarId, depth, bubble, poseType, action, log } = (params || {}) as {
|
|
2168
|
+
toAvatarId?: string;
|
|
2169
|
+
depth?: number;
|
|
2170
|
+
bubble?: string;
|
|
2171
|
+
poseType?: PoseType;
|
|
2172
|
+
action?: string;
|
|
2173
|
+
log?: string;
|
|
2174
|
+
};
|
|
2175
|
+
if (typeof toAvatarId !== "string" || !toAvatarId.trim()) {
|
|
2176
|
+
return jsonResult({ success: false, error: "toAvatarId is required" });
|
|
2177
|
+
}
|
|
2178
|
+
if (typeof depth !== "number" || depth < 0) {
|
|
2179
|
+
return jsonResult({ success: false, error: "depth must be a non-negative number" });
|
|
2180
|
+
}
|
|
2181
|
+
if (typeof bubble !== "string" || !bubble.trim()) {
|
|
2182
|
+
return jsonResult({ success: false, error: "bubble is required" });
|
|
2183
|
+
}
|
|
2184
|
+
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
2185
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
2186
|
+
}
|
|
2187
|
+
try {
|
|
2188
|
+
let playback: ActionPlayback | undefined;
|
|
2189
|
+
if (poseType && action) {
|
|
2190
|
+
const actionDef = getActionDefinition(poseType, action);
|
|
2191
|
+
playback = getActionPlayback(actionDef);
|
|
2192
|
+
}
|
|
2193
|
+
const ack = await service.sendBotMessage(toAvatarId.trim(), depth, bubble.trim(), {
|
|
2194
|
+
poseType,
|
|
2195
|
+
action: action?.trim(),
|
|
2196
|
+
log: log?.trim(),
|
|
2197
|
+
playback,
|
|
2198
|
+
});
|
|
2199
|
+
return jsonResult({ success: true, ...ack });
|
|
2200
|
+
} catch (error) {
|
|
2201
|
+
return jsonResult({ success: false, error: `Failed to send bot message: ${error}` });
|
|
1734
2202
|
}
|
|
1735
2203
|
},
|
|
1736
|
-
}))
|
|
2204
|
+
}), { name: "kichi_bot_message" });
|
|
1737
2205
|
|
|
1738
2206
|
},
|
|
1739
2207
|
};
|
|
1740
2208
|
|
|
1741
2209
|
export default plugin;
|
|
1742
|
-
|