@yahaha-studio/kichi-forwarder 0.1.2-beta.2 → 0.1.2-beta.20
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 +1876 -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 +614 -158
- package/openclaw.plugin.json +18 -1
- package/package.json +16 -7
- package/skills/kichi-forwarder/SKILL.md +44 -17
- package/skills/kichi-forwarder/references/error.md +3 -11
- package/skills/kichi-forwarder/references/heartbeat.md +5 -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,13 @@ 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;
|
|
58
71
|
let cachedStaticConfig: KichiStaticConfig | null = null;
|
|
59
72
|
let cachedStaticConfigMtime = 0;
|
|
60
73
|
|
|
74
|
+
type AvatarStatusName = typeof AVATAR_STATUSES[number];
|
|
61
75
|
type IdlePlanPomodoroPhase = typeof IDLE_PLAN_POMODORO_PHASES[number];
|
|
62
76
|
type IdlePlanAction = {
|
|
63
77
|
poseType: PoseType;
|
|
@@ -75,6 +89,7 @@ type IdlePlan = {
|
|
|
75
89
|
name: string;
|
|
76
90
|
purpose: string;
|
|
77
91
|
pomodoroPhase: IdlePlanPomodoroPhase;
|
|
92
|
+
avatarStatus: AvatarStatus;
|
|
78
93
|
durationSeconds: number;
|
|
79
94
|
actions: IdlePlanAction[];
|
|
80
95
|
}>;
|
|
@@ -249,6 +264,27 @@ function resolveEnvironmentHost(environment: KichiEnvironment): { host?: string;
|
|
|
249
264
|
return { error: `environment "${environment}" has no configured host — update config/environments.json first` };
|
|
250
265
|
}
|
|
251
266
|
|
|
267
|
+
function resolveJoinEnvironmentHost(params: {
|
|
268
|
+
environment?: unknown;
|
|
269
|
+
host?: unknown;
|
|
270
|
+
}): { environment?: KichiEnvironment; host?: string; error?: string } {
|
|
271
|
+
if (!isKichiEnvironment(params.environment)) {
|
|
272
|
+
return { error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` };
|
|
273
|
+
}
|
|
274
|
+
if (params.environment === "test") {
|
|
275
|
+
const testHost = typeof params.host === "string" ? params.host.trim() : "";
|
|
276
|
+
if (!testHost) {
|
|
277
|
+
return { error: "host is required for the test environment" };
|
|
278
|
+
}
|
|
279
|
+
return { environment: params.environment, host: testHost };
|
|
280
|
+
}
|
|
281
|
+
const resolved = resolveEnvironmentHost(params.environment);
|
|
282
|
+
if (resolved.error) {
|
|
283
|
+
return { environment: params.environment, error: resolved.error };
|
|
284
|
+
}
|
|
285
|
+
return { environment: params.environment, host: resolved.host };
|
|
286
|
+
}
|
|
287
|
+
|
|
252
288
|
function sendStatusUpdate(service: KichiForwarderService, status: ActionResult): void {
|
|
253
289
|
const actionDefinition = getActionDefinition(status.poseType, status.action);
|
|
254
290
|
service.sendStatus(
|
|
@@ -257,6 +293,8 @@ function sendStatusUpdate(service: KichiForwarderService, status: ActionResult):
|
|
|
257
293
|
status.bubble || status.action,
|
|
258
294
|
typeof status.log === "string" ? status.log.trim() : "",
|
|
259
295
|
getActionPlayback(actionDefinition),
|
|
296
|
+
status.avatarStatus,
|
|
297
|
+
status.propId,
|
|
260
298
|
);
|
|
261
299
|
}
|
|
262
300
|
|
|
@@ -448,6 +486,7 @@ function notifyMessageReceived(
|
|
|
448
486
|
service: KichiForwarderService,
|
|
449
487
|
content: string,
|
|
450
488
|
): void {
|
|
489
|
+
service.recordSmsLastMessageReceivedAt();
|
|
451
490
|
const connected = service.isConnected();
|
|
452
491
|
const hasIdentity = service.hasValidIdentity();
|
|
453
492
|
api.logger.debug(`[kichi:${service.getAgentId()}] inbound sync fired (connected=${connected}, hasIdentity=${hasIdentity})`);
|
|
@@ -635,6 +674,13 @@ function isIdlePlanPomodoroPhase(value: unknown): value is IdlePlanPomodoroPhase
|
|
|
635
674
|
return IDLE_PLAN_POMODORO_PHASES.includes(String(value) as IdlePlanPomodoroPhase);
|
|
636
675
|
}
|
|
637
676
|
|
|
677
|
+
function normalizeAvatarStatus(value: unknown, fieldPath: string): { avatarStatus?: AvatarStatus; error?: string } {
|
|
678
|
+
if (typeof value !== "string" || !AVATAR_STATUSES.includes(value as AvatarStatusName)) {
|
|
679
|
+
return { error: `${fieldPath} must be one of: ${AVATAR_STATUSES.join(", ")}` };
|
|
680
|
+
}
|
|
681
|
+
return { avatarStatus: value as AvatarStatus };
|
|
682
|
+
}
|
|
683
|
+
|
|
638
684
|
function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: string } {
|
|
639
685
|
if (!isPlainObject(value)) {
|
|
640
686
|
return { error: "idle plan payload must be an object" };
|
|
@@ -670,6 +716,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
|
|
|
670
716
|
const name = rawStage.name;
|
|
671
717
|
const purpose = rawStage.purpose;
|
|
672
718
|
const pomodoroPhase = rawStage.pomodoroPhase;
|
|
719
|
+
const avatarStatus = rawStage.avatarStatus;
|
|
673
720
|
const durationSeconds = rawStage.durationSeconds;
|
|
674
721
|
const actions = rawStage.actions;
|
|
675
722
|
|
|
@@ -684,6 +731,10 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
|
|
|
684
731
|
error: `stages[${stageIndex}].pomodoroPhase must be one of: ${IDLE_PLAN_POMODORO_PHASES.join(", ")}`,
|
|
685
732
|
};
|
|
686
733
|
}
|
|
734
|
+
const normalizedAvatarStatus = normalizeAvatarStatus(avatarStatus, `stages[${stageIndex}].avatarStatus`);
|
|
735
|
+
if (normalizedAvatarStatus.error || normalizedAvatarStatus.avatarStatus === undefined) {
|
|
736
|
+
return { error: normalizedAvatarStatus.error ?? `stages[${stageIndex}].avatarStatus is invalid` };
|
|
737
|
+
}
|
|
687
738
|
if (!isPositiveInteger(durationSeconds)) {
|
|
688
739
|
return { error: `stages[${stageIndex}].durationSeconds must be a positive integer` };
|
|
689
740
|
}
|
|
@@ -705,6 +756,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
|
|
|
705
756
|
const actionDurationSeconds = rawAction.durationSeconds;
|
|
706
757
|
const bubble = rawAction.bubble;
|
|
707
758
|
const log = rawAction.log;
|
|
759
|
+
const propId = rawAction.propId;
|
|
708
760
|
|
|
709
761
|
if (!["stand", "sit", "lay", "floor"].includes(String(poseType))) {
|
|
710
762
|
return {
|
|
@@ -752,6 +804,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
|
|
|
752
804
|
durationSeconds: actionDurationSeconds,
|
|
753
805
|
bubble: bubble.trim(),
|
|
754
806
|
...(typeof log === "string" && log.trim() ? { log: log.trim() } : {}),
|
|
807
|
+
...(typeof propId === "string" && propId.trim() ? { propId: propId.trim() } : {}),
|
|
755
808
|
});
|
|
756
809
|
}
|
|
757
810
|
|
|
@@ -766,6 +819,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
|
|
|
766
819
|
name: name.trim(),
|
|
767
820
|
purpose: purpose.trim(),
|
|
768
821
|
pomodoroPhase,
|
|
822
|
+
avatarStatus: normalizedAvatarStatus.avatarStatus,
|
|
769
823
|
durationSeconds,
|
|
770
824
|
actions: normalizedActions,
|
|
771
825
|
});
|
|
@@ -985,18 +1039,32 @@ function formatActionList(actions: ActionDefinition[], playback: ActionPlayback[
|
|
|
985
1039
|
.join(", ");
|
|
986
1040
|
}
|
|
987
1041
|
|
|
988
|
-
function buildKichiActionDescription(): string {
|
|
1042
|
+
function buildKichiActionDescription(service?: KichiForwarderService): string {
|
|
989
1043
|
const actions = loadStaticConfig().actions;
|
|
990
|
-
|
|
1044
|
+
const lines = [
|
|
991
1045
|
"Directly control the avatar inside Kichi World.",
|
|
992
1046
|
"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
1047
|
"For most work, prefer a sit pose and switch actions as the task moves between stages.",
|
|
1048
|
+
"Set avatarStatus to the current avatar status: Idle, Busy, Activities, or Break.",
|
|
994
1049
|
"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
1050
|
`stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
|
|
996
1051
|
`sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
|
|
997
1052
|
`lay actions: ${actions.lay.map((entry) => entry.name).join(", ")}`,
|
|
998
1053
|
`floor actions: ${actions.floor.map((entry) => entry.name).join(", ")}`,
|
|
999
|
-
]
|
|
1054
|
+
];
|
|
1055
|
+
|
|
1056
|
+
const roomContext = service?.getCachedRoomContext();
|
|
1057
|
+
const poseableProps = roomContext?.PoseableProps;
|
|
1058
|
+
if (Array.isArray(poseableProps) && poseableProps.length > 0) {
|
|
1059
|
+
lines.push(
|
|
1060
|
+
"",
|
|
1061
|
+
"Cached RoomContext.PoseableProps (from last kichi_query_status):",
|
|
1062
|
+
JSON.stringify(poseableProps),
|
|
1063
|
+
"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.",
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
return lines.join("\n");
|
|
1000
1068
|
}
|
|
1001
1069
|
|
|
1002
1070
|
function buildKichiIdlePlanDescription(): string {
|
|
@@ -1006,12 +1074,14 @@ function buildKichiIdlePlanDescription(): string {
|
|
|
1006
1074
|
"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
1075
|
"Build the plan in this order.",
|
|
1008
1076
|
"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.
|
|
1077
|
+
"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
1078
|
"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
1079
|
"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.
|
|
1080
|
+
"5. Set each stage avatarStatus to the avatar status for that stage: Idle, Busy, Activities, or Break.",
|
|
1081
|
+
"6. Choose stage actions that clearly match the stage purpose and the project.",
|
|
1082
|
+
"7. Write each action bubble as the current presented state, not a next step, plan, or instruction.",
|
|
1083
|
+
"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.",
|
|
1084
|
+
"Use your memory to recall what you did in past heartbeats and to stay consistent with your established personality and interests.",
|
|
1015
1085
|
"Use the same language as the current conversation for goal, purpose, bubble, and log.",
|
|
1016
1086
|
`stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
|
|
1017
1087
|
`sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
|
|
@@ -1023,6 +1093,10 @@ function buildKichiIdlePlanDescription(): string {
|
|
|
1023
1093
|
function buildKichiPrompt(): string {
|
|
1024
1094
|
return [
|
|
1025
1095
|
"Kichi avatar control and status sync are available via `kichi_action` and `kichi_clock`.",
|
|
1096
|
+
"",
|
|
1097
|
+
"IMAGE GENERATION RULES FOR SELFIES AND AVATAR APPEARANCE:",
|
|
1098
|
+
"- 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
|
+
"",
|
|
1026
1100
|
"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
1101
|
"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
1102
|
"",
|
|
@@ -1031,27 +1105,16 @@ function buildKichiPrompt(): string {
|
|
|
1031
1105
|
"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
1106
|
"3. Task end: call BEFORE final reply. Use the order `kichi_action` -> reply.",
|
|
1033
1107
|
"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.",
|
|
1108
|
+
"avatarStatus: set the current avatar status as Idle, Busy, Activities, or Break.",
|
|
1034
1109
|
"",
|
|
1035
1110
|
"kichi_clock: set countDown for tasks with 2+ steps or >10s work. Skip for quick one-shots.",
|
|
1036
1111
|
"",
|
|
1112
|
+
"When sending a bot message, do NOT call kichi_action separately.",
|
|
1113
|
+
"",
|
|
1037
1114
|
"User opt-out, Kichi config/test work, and explicit pose requests take priority over sync.",
|
|
1038
1115
|
].join("\n");
|
|
1039
1116
|
}
|
|
1040
1117
|
|
|
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
1118
|
|
|
1056
1119
|
const GLOBAL_RUNTIME_MANAGER_KEY = "__kichi_forwarder_runtime_manager__";
|
|
1057
1120
|
|
|
@@ -1070,6 +1133,10 @@ function getRuntimeManager(logger: OpenClawPluginApi["logger"]): KichiRuntimeMan
|
|
|
1070
1133
|
return runtimeManager;
|
|
1071
1134
|
}
|
|
1072
1135
|
|
|
1136
|
+
const BOT_MESSAGE_MAX_DEPTH = 5;
|
|
1137
|
+
const BOT_MESSAGE_COOLDOWN_MS = 5_000;
|
|
1138
|
+
const botMessageCooldowns = new Map<string, number>();
|
|
1139
|
+
|
|
1073
1140
|
const plugin = {
|
|
1074
1141
|
id: "kichi-forwarder",
|
|
1075
1142
|
name: "Kichi Forwarder",
|
|
@@ -1077,18 +1144,61 @@ const plugin = {
|
|
|
1077
1144
|
|
|
1078
1145
|
register(api: OpenClawPluginApi) {
|
|
1079
1146
|
const runtimeManager = getRuntimeManager(api.logger);
|
|
1147
|
+
|
|
1148
|
+
runtimeManager.setEnvironmentHostResolver((environment) => {
|
|
1149
|
+
const config = loadEnvironmentsConfig();
|
|
1150
|
+
const host = config[environment];
|
|
1151
|
+
return typeof host === "string" && host.trim() ? host : null;
|
|
1152
|
+
});
|
|
1153
|
+
|
|
1080
1154
|
registerPluginHooks(api, runtimeManager);
|
|
1081
1155
|
const musicTitleEnum = getMusicTitleEnum();
|
|
1082
1156
|
|
|
1157
|
+
runtimeManager.setBotMessageHandler((service, msg) => {
|
|
1158
|
+
if (msg.depth >= BOT_MESSAGE_MAX_DEPTH) {
|
|
1159
|
+
api.logger.info(`[kichi:${service.getAgentId()}] bot_message depth=${msg.depth} >= max=${BOT_MESSAGE_MAX_DEPTH}, ignoring`);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
const now = Date.now();
|
|
1163
|
+
const cooldownKey = `${service.getAgentId()}:${msg.from}`;
|
|
1164
|
+
const lastReply = botMessageCooldowns.get(cooldownKey) ?? 0;
|
|
1165
|
+
if (now - lastReply < BOT_MESSAGE_COOLDOWN_MS) return;
|
|
1166
|
+
botMessageCooldowns.set(cooldownKey, now);
|
|
1167
|
+
const sessionKey = `agent:${service.getAgentId()}:default`;
|
|
1168
|
+
const history: BotMessageHistoryEntry[] = [
|
|
1169
|
+
...(msg.history ?? []),
|
|
1170
|
+
{ from: msg.from, fromName: msg.fromName, bubble: msg.bubble },
|
|
1171
|
+
];
|
|
1172
|
+
const historyLines = history.map((h) => `${h.fromName}: "${h.bubble}"`);
|
|
1173
|
+
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.`;
|
|
1174
|
+
agentCommandFromIngress({
|
|
1175
|
+
message,
|
|
1176
|
+
sessionKey,
|
|
1177
|
+
agentId: service.getAgentId(),
|
|
1178
|
+
senderIsOwner: false,
|
|
1179
|
+
allowModelOverride: false,
|
|
1180
|
+
deliver: false,
|
|
1181
|
+
}).then((result) => {
|
|
1182
|
+
const replyText = (result.payloads ?? [])
|
|
1183
|
+
.map((p: { text?: string }) => p.text)
|
|
1184
|
+
.filter((t): t is string => typeof t === "string" && t.trim().length > 0)
|
|
1185
|
+
.join(" ")
|
|
1186
|
+
.trim();
|
|
1187
|
+
if (!replyText) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
service.sendBotMessage(msg.from, msg.depth + 1, replyText, { history }).catch((sendErr) => {
|
|
1191
|
+
api.logger.warn(`[kichi:${service.getAgentId()}] bot_message send failed: ${sendErr}`);
|
|
1192
|
+
});
|
|
1193
|
+
}).catch((err) => {
|
|
1194
|
+
api.logger.warn(`[kichi:${service.getAgentId()}] bot_message agent run failed: ${err}`);
|
|
1195
|
+
});
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1083
1198
|
api.registerService({
|
|
1084
1199
|
id: "kichi-forwarder",
|
|
1085
1200
|
start: (ctx) => {
|
|
1086
1201
|
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
1202
|
runtimeManager.initializeStartupRuntimes();
|
|
1093
1203
|
},
|
|
1094
1204
|
stop: () => {
|
|
@@ -1100,75 +1210,154 @@ const plugin = {
|
|
|
1100
1210
|
},
|
|
1101
1211
|
});
|
|
1102
1212
|
|
|
1103
|
-
api.registerTool(
|
|
1213
|
+
api.registerTool((ctx) => ({
|
|
1104
1214
|
name: "kichi_join",
|
|
1105
|
-
|
|
1215
|
+
label: "kichi_join",
|
|
1216
|
+
description:
|
|
1217
|
+
"Join Kichi world in the target environment with avatarId, the current bot name, a short bio, and personality tags. For test, pass host.",
|
|
1106
1218
|
parameters: {
|
|
1107
1219
|
type: "object",
|
|
1108
1220
|
properties: {
|
|
1109
1221
|
avatarId: { type: "string", description: "Avatar ID to join Kichi world" },
|
|
1222
|
+
environment: {
|
|
1223
|
+
type: "string",
|
|
1224
|
+
enum: VALID_ENVIRONMENTS,
|
|
1225
|
+
description:
|
|
1226
|
+
"Target environment. kichi_join switches to this environment before joining.",
|
|
1227
|
+
},
|
|
1228
|
+
host: {
|
|
1229
|
+
type: "string",
|
|
1230
|
+
description: "Test host, required when environment is test and ignored otherwise",
|
|
1231
|
+
},
|
|
1110
1232
|
botName: {
|
|
1111
1233
|
type: "string",
|
|
1112
1234
|
description: "Current bot name to include in the join message",
|
|
1113
1235
|
},
|
|
1114
1236
|
bio: {
|
|
1115
1237
|
type: "string",
|
|
1116
|
-
description: "Short bio covering
|
|
1238
|
+
description: "Short bio extracted from SOUL.md, covering persona and idle plan goals if present",
|
|
1117
1239
|
},
|
|
1118
1240
|
tags: {
|
|
1119
1241
|
type: "array",
|
|
1120
1242
|
description: "Optional list of OpenClaw self-perceived personality tags",
|
|
1121
1243
|
items: { type: "string" },
|
|
1122
1244
|
},
|
|
1245
|
+
source: {
|
|
1246
|
+
type: "string",
|
|
1247
|
+
description: "Optional join source identifier. Defaults to Kichi World join-source.json, then openclaw.",
|
|
1248
|
+
},
|
|
1123
1249
|
},
|
|
1124
|
-
required: ["botName", "bio"],
|
|
1250
|
+
required: ["environment", "avatarId", "botName", "bio"],
|
|
1125
1251
|
},
|
|
1126
1252
|
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;
|
|
1253
|
+
const locator = resolveToolLocator(ctx);
|
|
1254
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1255
|
+
if (!agentId) {
|
|
1256
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1135
1257
|
}
|
|
1136
|
-
|
|
1137
|
-
|
|
1258
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1259
|
+
const p = params as {
|
|
1260
|
+
avatarId?: string;
|
|
1261
|
+
environment?: unknown;
|
|
1262
|
+
host?: unknown;
|
|
1263
|
+
botName?: string;
|
|
1264
|
+
bio?: string;
|
|
1265
|
+
source?: unknown;
|
|
1266
|
+
tags?: unknown;
|
|
1267
|
+
} | null;
|
|
1268
|
+
const target = resolveJoinEnvironmentHost({
|
|
1269
|
+
environment: p?.environment,
|
|
1270
|
+
host: p?.host,
|
|
1271
|
+
});
|
|
1272
|
+
if (target.error) {
|
|
1273
|
+
return jsonResult({ success: false, error: target.error });
|
|
1274
|
+
}
|
|
1275
|
+
const currentStatus = service.getConnectionStatus();
|
|
1276
|
+
let avatarId = p?.avatarId;
|
|
1277
|
+
if (!avatarId && currentStatus.host === target.host) {
|
|
1278
|
+
avatarId = service.readSavedAvatarId() ?? undefined;
|
|
1138
1279
|
}
|
|
1280
|
+
const botName = p?.botName?.trim();
|
|
1281
|
+
const bio = p?.bio?.trim();
|
|
1282
|
+
const rawSource = p?.source;
|
|
1283
|
+
const { tags, error: tagsError } = normalizeJoinTags(
|
|
1284
|
+
p?.tags,
|
|
1285
|
+
);
|
|
1139
1286
|
if (!botName) {
|
|
1140
|
-
return { success: false, error: "No botName" };
|
|
1287
|
+
return jsonResult({ success: false, error: "No botName" });
|
|
1141
1288
|
}
|
|
1142
1289
|
if (!bio) {
|
|
1143
|
-
return { success: false, error: "No bio" };
|
|
1290
|
+
return jsonResult({ success: false, error: "No bio" });
|
|
1291
|
+
}
|
|
1292
|
+
let source: string | null | undefined;
|
|
1293
|
+
try {
|
|
1294
|
+
source = rawSource === undefined
|
|
1295
|
+
? service.readConfiguredJoinSource() ?? "openclaw"
|
|
1296
|
+
: trimOptionalString(rawSource);
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
return jsonResult({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
1299
|
+
}
|
|
1300
|
+
if (!source) {
|
|
1301
|
+
return jsonResult({ success: false, error: "source must be a non-empty string" });
|
|
1144
1302
|
}
|
|
1145
1303
|
if (tagsError) {
|
|
1146
|
-
return { success: false, error: tagsError };
|
|
1304
|
+
return jsonResult({ success: false, error: tagsError });
|
|
1305
|
+
}
|
|
1306
|
+
let leaveStatus;
|
|
1307
|
+
const shouldLeaveCurrentConnection = currentStatus.connected && currentStatus.hasAuthKey && (
|
|
1308
|
+
(!!currentStatus.host && currentStatus.host !== target.host) ||
|
|
1309
|
+
(currentStatus.host === target.host && !!currentStatus.avatarId && !!avatarId && currentStatus.avatarId !== avatarId)
|
|
1310
|
+
);
|
|
1311
|
+
if (shouldLeaveCurrentConnection) {
|
|
1312
|
+
try {
|
|
1313
|
+
leaveStatus = await service.leave();
|
|
1314
|
+
} catch (err) {
|
|
1315
|
+
leaveStatus = {
|
|
1316
|
+
success: false,
|
|
1317
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
let switchStatus;
|
|
1322
|
+
if (target.environment && target.host && service.getCurrentHost() !== target.host) {
|
|
1323
|
+
switchStatus = await service.switchHost(target.host, target.environment);
|
|
1324
|
+
}
|
|
1325
|
+
if (!avatarId) {
|
|
1326
|
+
avatarId = service.readSavedAvatarId() ?? undefined;
|
|
1327
|
+
}
|
|
1328
|
+
if (!avatarId) {
|
|
1329
|
+
return jsonResult({ success: false, error: "No avatarId" });
|
|
1147
1330
|
}
|
|
1148
|
-
const result = await service.join(avatarId, botName, bio, tags ?? []);
|
|
1331
|
+
const result = await service.join(avatarId, botName, bio, tags ?? [], source);
|
|
1149
1332
|
if (result.success) {
|
|
1150
|
-
return {
|
|
1333
|
+
return jsonResult({
|
|
1334
|
+
success: true,
|
|
1335
|
+
authKey: result.authKey,
|
|
1336
|
+
...(target.environment ? { environment: target.environment } : {}),
|
|
1337
|
+
...(target.host ? { host: target.host } : {}),
|
|
1338
|
+
...(switchStatus ? { switchStatus } : {}),
|
|
1339
|
+
...(leaveStatus ? { leaveStatus } : {}),
|
|
1340
|
+
});
|
|
1151
1341
|
}
|
|
1152
|
-
|
|
1342
|
+
const failure = result as { success: false; error: string; errorCode?: string; errorMessage?: string };
|
|
1343
|
+
return jsonResult({
|
|
1153
1344
|
success: false,
|
|
1154
|
-
error:
|
|
1155
|
-
...(
|
|
1156
|
-
...(
|
|
1157
|
-
|
|
1345
|
+
error: failure.error,
|
|
1346
|
+
...(target.environment ? { environment: target.environment } : {}),
|
|
1347
|
+
...(target.host ? { host: target.host } : {}),
|
|
1348
|
+
...(switchStatus ? { switchStatus } : {}),
|
|
1349
|
+
...(leaveStatus ? { leaveStatus } : {}),
|
|
1350
|
+
...(failure.errorCode ? { errorCode: failure.errorCode } : {}),
|
|
1351
|
+
...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
|
|
1352
|
+
});
|
|
1158
1353
|
},
|
|
1159
|
-
}))
|
|
1354
|
+
}), { name: "kichi_join" });
|
|
1160
1355
|
|
|
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 ({
|
|
1356
|
+
api.registerTool((ctx) => ({
|
|
1169
1357
|
name: "kichi_switch_host",
|
|
1358
|
+
label: "kichi_switch_host",
|
|
1170
1359
|
description:
|
|
1171
|
-
"Switch Kichi runtime environment and reconnect immediately without restarting the gateway.
|
|
1360
|
+
"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
1361
|
parameters: {
|
|
1173
1362
|
type: "object",
|
|
1174
1363
|
properties: {
|
|
@@ -1177,79 +1366,125 @@ const plugin = {
|
|
|
1177
1366
|
enum: VALID_ENVIRONMENTS,
|
|
1178
1367
|
description: "Target environment: steam, steam-playtest, or test",
|
|
1179
1368
|
},
|
|
1369
|
+
host: {
|
|
1370
|
+
type: "string",
|
|
1371
|
+
description: "Test host (required for test environment, ignored otherwise)",
|
|
1372
|
+
},
|
|
1180
1373
|
},
|
|
1181
1374
|
required: ["environment"],
|
|
1182
1375
|
},
|
|
1183
1376
|
execute: async (_toolCallId, params) => {
|
|
1184
|
-
const
|
|
1377
|
+
const locator = resolveToolLocator(ctx);
|
|
1378
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1379
|
+
if (!agentId) {
|
|
1380
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1381
|
+
}
|
|
1382
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1383
|
+
const p = params as { environment?: unknown; host?: unknown } | null;
|
|
1384
|
+
const environment = p?.environment;
|
|
1185
1385
|
if (!isKichiEnvironment(environment)) {
|
|
1186
|
-
return { success: false, error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` };
|
|
1386
|
+
return jsonResult({ success: false, error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` });
|
|
1187
1387
|
}
|
|
1188
1388
|
|
|
1189
|
-
|
|
1190
|
-
if (
|
|
1191
|
-
|
|
1389
|
+
let targetHost: string;
|
|
1390
|
+
if (environment === "test") {
|
|
1391
|
+
const testHost = typeof p?.host === "string" ? p.host.trim() : "";
|
|
1392
|
+
if (!testHost) {
|
|
1393
|
+
return jsonResult({ success: false, error: "host is required for the test environment" });
|
|
1394
|
+
}
|
|
1395
|
+
targetHost = testHost;
|
|
1396
|
+
} else {
|
|
1397
|
+
const resolved = resolveEnvironmentHost(environment);
|
|
1398
|
+
if (resolved.error) {
|
|
1399
|
+
return jsonResult({ success: false, error: resolved.error });
|
|
1400
|
+
}
|
|
1401
|
+
targetHost = resolved.host!;
|
|
1192
1402
|
}
|
|
1193
1403
|
|
|
1194
|
-
const status = await service.switchHost(
|
|
1195
|
-
return {
|
|
1404
|
+
const status = await service.switchHost(targetHost, environment);
|
|
1405
|
+
return jsonResult({
|
|
1196
1406
|
success: true,
|
|
1197
1407
|
environment,
|
|
1198
|
-
host:
|
|
1408
|
+
host: targetHost,
|
|
1199
1409
|
status,
|
|
1200
|
-
};
|
|
1410
|
+
});
|
|
1201
1411
|
},
|
|
1202
|
-
|
|
1203
|
-
});
|
|
1412
|
+
}), { name: "kichi_switch_host" });
|
|
1204
1413
|
|
|
1205
|
-
api.registerTool(
|
|
1414
|
+
api.registerTool((ctx) => ({
|
|
1206
1415
|
name: "kichi_rejoin",
|
|
1416
|
+
label: "kichi_rejoin",
|
|
1207
1417
|
description:
|
|
1208
1418
|
"Request an immediate rejoin attempt with saved avatarId/authKey. Rejoin is also sent automatically after reconnect.",
|
|
1209
1419
|
parameters: { type: "object", properties: {} },
|
|
1210
|
-
execute: async () => {
|
|
1420
|
+
execute: async (_toolCallId, _params) => {
|
|
1421
|
+
const locator = resolveToolLocator(ctx);
|
|
1422
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1423
|
+
if (!agentId) {
|
|
1424
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1425
|
+
}
|
|
1426
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1211
1427
|
const result = service.requestRejoin();
|
|
1212
|
-
return {
|
|
1428
|
+
return jsonResult({
|
|
1213
1429
|
success: result.accepted,
|
|
1214
1430
|
...result,
|
|
1215
1431
|
status: service.getConnectionStatus(),
|
|
1216
|
-
};
|
|
1432
|
+
});
|
|
1217
1433
|
},
|
|
1218
|
-
}))
|
|
1434
|
+
}), { name: "kichi_rejoin" });
|
|
1219
1435
|
|
|
1220
|
-
api.registerTool(
|
|
1436
|
+
api.registerTool((ctx) => ({
|
|
1221
1437
|
name: "kichi_leave",
|
|
1438
|
+
label: "kichi_leave",
|
|
1222
1439
|
description: "Leave Kichi world",
|
|
1223
1440
|
parameters: { type: "object", properties: {} },
|
|
1224
|
-
execute: async () => {
|
|
1441
|
+
execute: async (_toolCallId, _params) => {
|
|
1442
|
+
const locator = resolveToolLocator(ctx);
|
|
1443
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1444
|
+
if (!agentId) {
|
|
1445
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1446
|
+
}
|
|
1447
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1225
1448
|
const result = await service.leave();
|
|
1226
1449
|
if (result.success) {
|
|
1227
|
-
return { success: true };
|
|
1450
|
+
return jsonResult({ success: true });
|
|
1228
1451
|
}
|
|
1229
|
-
|
|
1452
|
+
const failure = result as { success: false; error: string; errorCode?: string; errorMessage?: string };
|
|
1453
|
+
return jsonResult({
|
|
1230
1454
|
success: false,
|
|
1231
|
-
error:
|
|
1232
|
-
...(
|
|
1233
|
-
...(
|
|
1234
|
-
};
|
|
1455
|
+
error: failure.error,
|
|
1456
|
+
...(failure.errorCode ? { errorCode: failure.errorCode } : {}),
|
|
1457
|
+
...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
|
|
1458
|
+
});
|
|
1235
1459
|
},
|
|
1236
|
-
}))
|
|
1460
|
+
}), { name: "kichi_leave" });
|
|
1237
1461
|
|
|
1238
|
-
api.registerTool(
|
|
1462
|
+
api.registerTool((ctx) => ({
|
|
1239
1463
|
name: "kichi_connection_status",
|
|
1464
|
+
label: "kichi_connection_status",
|
|
1240
1465
|
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
1466
|
parameters: { type: "object", properties: {} },
|
|
1242
|
-
execute: async () => {
|
|
1243
|
-
|
|
1467
|
+
execute: async (_toolCallId, _params) => {
|
|
1468
|
+
const locator = resolveToolLocator(ctx);
|
|
1469
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1470
|
+
if (!agentId) {
|
|
1471
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1472
|
+
}
|
|
1473
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1474
|
+
return jsonResult({
|
|
1244
1475
|
success: true,
|
|
1245
1476
|
status: service.getConnectionStatus(),
|
|
1246
|
-
};
|
|
1477
|
+
});
|
|
1247
1478
|
},
|
|
1248
|
-
}))
|
|
1479
|
+
}), { name: "kichi_connection_status" });
|
|
1249
1480
|
|
|
1250
|
-
api.registerTool(
|
|
1481
|
+
api.registerTool((ctx) => {
|
|
1482
|
+
const locator = resolveToolLocator(ctx);
|
|
1483
|
+
const existingService = runtimeManager.getRuntime(locator);
|
|
1484
|
+
return ({
|
|
1251
1485
|
name: "kichi_action",
|
|
1252
|
-
|
|
1486
|
+
label: "kichi_action",
|
|
1487
|
+
description: buildKichiActionDescription(existingService ?? undefined),
|
|
1253
1488
|
parameters: {
|
|
1254
1489
|
type: "object",
|
|
1255
1490
|
properties: {
|
|
@@ -1259,6 +1494,11 @@ const plugin = {
|
|
|
1259
1494
|
description: "Action name for the selected pose (for example Sit Nicely, Typing with Keyboard, Reading, High Five, or Meditate)",
|
|
1260
1495
|
},
|
|
1261
1496
|
bubble: { type: "string", description: "Optional bubble text to display (max 5 words)" },
|
|
1497
|
+
avatarStatus: {
|
|
1498
|
+
type: "string",
|
|
1499
|
+
description: "Current avatar status: Idle, Busy, Activities, or Break.",
|
|
1500
|
+
enum: [...AVATAR_STATUSES],
|
|
1501
|
+
},
|
|
1262
1502
|
log: {
|
|
1263
1503
|
type: "string",
|
|
1264
1504
|
description:
|
|
@@ -1269,39 +1509,56 @@ const plugin = {
|
|
|
1269
1509
|
description:
|
|
1270
1510
|
"Set true ONLY when the user explicitly requests a pose or action. Omit during routine sync steps.",
|
|
1271
1511
|
},
|
|
1512
|
+
propId: {
|
|
1513
|
+
type: "string",
|
|
1514
|
+
description:
|
|
1515
|
+
"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.",
|
|
1516
|
+
},
|
|
1272
1517
|
},
|
|
1273
|
-
required: ["poseType", "action"],
|
|
1518
|
+
required: ["poseType", "action", "avatarStatus"],
|
|
1274
1519
|
},
|
|
1275
1520
|
execute: async (_toolCallId, params) => {
|
|
1276
|
-
const
|
|
1521
|
+
const locator = resolveToolLocator(ctx);
|
|
1522
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1523
|
+
if (!agentId) {
|
|
1524
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1525
|
+
}
|
|
1526
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1527
|
+
const { poseType, action, bubble, avatarStatus, log, verify, propId } = (params || {}) as {
|
|
1277
1528
|
poseType?: string;
|
|
1278
1529
|
action?: string;
|
|
1279
1530
|
bubble?: string;
|
|
1531
|
+
avatarStatus?: unknown;
|
|
1280
1532
|
log?: string;
|
|
1281
1533
|
verify?: boolean;
|
|
1534
|
+
propId?: string;
|
|
1282
1535
|
};
|
|
1283
1536
|
if (!poseType || !action) {
|
|
1284
|
-
return { success: false, error: "poseType and action parameters are required" };
|
|
1537
|
+
return jsonResult({ success: false, error: "poseType and action parameters are required" });
|
|
1285
1538
|
}
|
|
1286
1539
|
if (!["stand", "sit", "lay", "floor"].includes(poseType)) {
|
|
1287
|
-
return {
|
|
1540
|
+
return jsonResult({
|
|
1288
1541
|
success: false,
|
|
1289
1542
|
error: `Invalid poseType: ${poseType}. Must be stand, sit, lay, or floor`,
|
|
1290
|
-
};
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
const normalizedAvatarStatus = normalizeAvatarStatus(avatarStatus, "avatarStatus");
|
|
1546
|
+
if (normalizedAvatarStatus.error || normalizedAvatarStatus.avatarStatus === undefined) {
|
|
1547
|
+
return jsonResult({ success: false, error: normalizedAvatarStatus.error ?? "avatarStatus is invalid" });
|
|
1291
1548
|
}
|
|
1292
1549
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1293
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1550
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1294
1551
|
}
|
|
1295
1552
|
|
|
1296
1553
|
const normalizedPoseType = poseType as PoseType;
|
|
1297
1554
|
const poseActions = loadStaticConfig().actions[normalizedPoseType];
|
|
1298
1555
|
const matched = poseActions.find((entry) => entry.name.toLowerCase() === action.toLowerCase());
|
|
1299
1556
|
if (!matched) {
|
|
1300
|
-
return {
|
|
1557
|
+
return jsonResult({
|
|
1301
1558
|
success: false,
|
|
1302
1559
|
error: `Unknown action "${action}" for poseType "${poseType}"`,
|
|
1303
1560
|
available: poseActions.map((entry) => entry.name),
|
|
1304
|
-
};
|
|
1561
|
+
});
|
|
1305
1562
|
}
|
|
1306
1563
|
|
|
1307
1564
|
const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : matched.name;
|
|
@@ -1311,15 +1568,21 @@ const plugin = {
|
|
|
1311
1568
|
if (verify) {
|
|
1312
1569
|
try {
|
|
1313
1570
|
const ack = await service.sendStatusVerified(
|
|
1314
|
-
normalizedPoseType,
|
|
1571
|
+
normalizedPoseType,
|
|
1572
|
+
matched.name,
|
|
1573
|
+
bubbleText,
|
|
1574
|
+
logText,
|
|
1575
|
+
playback,
|
|
1576
|
+
normalizedAvatarStatus.avatarStatus,
|
|
1577
|
+
propId,
|
|
1315
1578
|
);
|
|
1316
1579
|
if (ack.warning) {
|
|
1317
|
-
return {
|
|
1580
|
+
return jsonResult({
|
|
1318
1581
|
success: true,
|
|
1319
1582
|
requested: { poseType: normalizedPoseType, action: matched.name },
|
|
1320
1583
|
actual: { poseType: ack.poseType, action: ack.action },
|
|
1321
1584
|
warning: ack.warning,
|
|
1322
|
-
};
|
|
1585
|
+
});
|
|
1323
1586
|
}
|
|
1324
1587
|
} catch {
|
|
1325
1588
|
// Server not updated or timeout — fall through to normal success
|
|
@@ -1330,21 +1593,90 @@ const plugin = {
|
|
|
1330
1593
|
action: matched.name,
|
|
1331
1594
|
bubble: bubbleText,
|
|
1332
1595
|
log: logText,
|
|
1596
|
+
avatarStatus: normalizedAvatarStatus.avatarStatus,
|
|
1597
|
+
propId,
|
|
1333
1598
|
});
|
|
1334
1599
|
}
|
|
1335
1600
|
|
|
1336
|
-
return {
|
|
1601
|
+
return jsonResult({
|
|
1337
1602
|
success: true,
|
|
1338
1603
|
poseType: normalizedPoseType,
|
|
1339
1604
|
action: matched.name,
|
|
1340
1605
|
bubble: bubbleText,
|
|
1341
1606
|
log: logText,
|
|
1607
|
+
avatarStatus: normalizedAvatarStatus.avatarStatus,
|
|
1342
1608
|
playback,
|
|
1609
|
+
});
|
|
1610
|
+
},
|
|
1611
|
+
})}, { name: "kichi_action" });
|
|
1612
|
+
|
|
1613
|
+
api.registerTool((ctx) => ({
|
|
1614
|
+
name: "kichi_glance",
|
|
1615
|
+
label: "kichi_glance",
|
|
1616
|
+
description:
|
|
1617
|
+
"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.",
|
|
1618
|
+
parameters: {
|
|
1619
|
+
type: "object",
|
|
1620
|
+
properties: {
|
|
1621
|
+
requestId: {
|
|
1622
|
+
type: "string",
|
|
1623
|
+
description: "Optional client request ID for tracing. The websocket ack returns this ID.",
|
|
1624
|
+
},
|
|
1625
|
+
target: {
|
|
1626
|
+
type: "string",
|
|
1627
|
+
enum: ["camera"],
|
|
1628
|
+
description: "Glance target. The only supported target is camera.",
|
|
1629
|
+
},
|
|
1630
|
+
duration: {
|
|
1631
|
+
type: "number",
|
|
1632
|
+
description: "Optional glance duration in seconds. Defaults to 1.8.",
|
|
1633
|
+
},
|
|
1634
|
+
},
|
|
1635
|
+
},
|
|
1636
|
+
execute: async (_toolCallId, params) => {
|
|
1637
|
+
const locator = resolveToolLocator(ctx);
|
|
1638
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1639
|
+
if (!agentId) {
|
|
1640
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1641
|
+
}
|
|
1642
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1643
|
+
const { requestId, target, duration } = (params || {}) as {
|
|
1644
|
+
requestId?: unknown;
|
|
1645
|
+
target?: unknown;
|
|
1646
|
+
duration?: unknown;
|
|
1343
1647
|
};
|
|
1648
|
+
|
|
1649
|
+
if (requestId !== undefined && typeof requestId !== "string") {
|
|
1650
|
+
return jsonResult({ success: false, error: "requestId must be a string when provided" });
|
|
1651
|
+
}
|
|
1652
|
+
const normalizedTarget = target === undefined ? "camera" : target;
|
|
1653
|
+
if (normalizedTarget !== "camera") {
|
|
1654
|
+
return jsonResult({ success: false, error: "target must be camera" });
|
|
1655
|
+
}
|
|
1656
|
+
const normalizedDuration = duration === undefined ? DEFAULT_GLANCE_DURATION_SECONDS : duration;
|
|
1657
|
+
if (typeof normalizedDuration !== "number" || !Number.isFinite(normalizedDuration) || normalizedDuration <= 0) {
|
|
1658
|
+
return jsonResult({ success: false, error: "duration must be a positive finite number" });
|
|
1659
|
+
}
|
|
1660
|
+
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1661
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
try {
|
|
1665
|
+
const ack = await service.sendGlance(
|
|
1666
|
+
"camera",
|
|
1667
|
+
normalizedDuration,
|
|
1668
|
+
typeof requestId === "string" ? requestId : undefined,
|
|
1669
|
+
);
|
|
1670
|
+
return jsonResult({ success: true, ...ack });
|
|
1671
|
+
} catch (error) {
|
|
1672
|
+
return jsonResult({ success: false, error: `Failed to send glance: ${error}` });
|
|
1673
|
+
}
|
|
1344
1674
|
},
|
|
1345
|
-
}))
|
|
1346
|
-
|
|
1675
|
+
}), { name: "kichi_glance" });
|
|
1676
|
+
|
|
1677
|
+
api.registerTool((ctx) => ({
|
|
1347
1678
|
name: "kichi_idle_plan",
|
|
1679
|
+
label: "kichi_idle_plan",
|
|
1348
1680
|
description: buildKichiIdlePlanDescription(),
|
|
1349
1681
|
parameters: {
|
|
1350
1682
|
type: "object",
|
|
@@ -1380,6 +1712,11 @@ const plugin = {
|
|
|
1380
1712
|
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
1713
|
enum: [...IDLE_PLAN_POMODORO_PHASES],
|
|
1382
1714
|
},
|
|
1715
|
+
avatarStatus: {
|
|
1716
|
+
type: "string",
|
|
1717
|
+
description: "Avatar status for this stage: Idle, Busy, Activities, or Break.",
|
|
1718
|
+
enum: [...AVATAR_STATUSES],
|
|
1719
|
+
},
|
|
1383
1720
|
durationSeconds: {
|
|
1384
1721
|
type: "number",
|
|
1385
1722
|
description: "Required duration in seconds for this stage.",
|
|
@@ -1410,24 +1747,34 @@ const plugin = {
|
|
|
1410
1747
|
type: "string",
|
|
1411
1748
|
description: "Optional log content for this action. Use the same language as the current conversation.",
|
|
1412
1749
|
},
|
|
1750
|
+
propId: {
|
|
1751
|
+
type: "string",
|
|
1752
|
+
description: "Optional poseable prop ID from RoomContext.PoseableProps. When specified, the avatar is seated at this prop.",
|
|
1753
|
+
},
|
|
1413
1754
|
},
|
|
1414
1755
|
required: ["poseType", "action", "durationSeconds", "bubble"],
|
|
1415
1756
|
},
|
|
1416
1757
|
},
|
|
1417
1758
|
},
|
|
1418
|
-
required: ["name", "purpose", "pomodoroPhase", "durationSeconds", "actions"],
|
|
1759
|
+
required: ["name", "purpose", "pomodoroPhase", "avatarStatus", "durationSeconds", "actions"],
|
|
1419
1760
|
},
|
|
1420
1761
|
},
|
|
1421
1762
|
},
|
|
1422
1763
|
required: ["heartbeatIntervalSeconds", "goal", "stages"],
|
|
1423
1764
|
},
|
|
1424
1765
|
execute: async (_toolCallId, params) => {
|
|
1766
|
+
const locator = resolveToolLocator(ctx);
|
|
1767
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1768
|
+
if (!agentId) {
|
|
1769
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1770
|
+
}
|
|
1771
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1425
1772
|
const { idlePlan, error } = normalizeIdlePlan(params);
|
|
1426
1773
|
if (!idlePlan) {
|
|
1427
|
-
return { success: false, error: error ?? "Invalid idle plan payload" };
|
|
1774
|
+
return jsonResult({ success: false, error: error ?? "Invalid idle plan payload" });
|
|
1428
1775
|
}
|
|
1429
1776
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1430
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1777
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1431
1778
|
}
|
|
1432
1779
|
const sent = service.sendIdlePlan({
|
|
1433
1780
|
...(idlePlan.requestId ? { requestId: idlePlan.requestId } : {}),
|
|
@@ -1436,20 +1783,21 @@ const plugin = {
|
|
|
1436
1783
|
stages: idlePlan.stages,
|
|
1437
1784
|
});
|
|
1438
1785
|
if (!sent) {
|
|
1439
|
-
return { success: false, error: "Failed to send idle plan payload" };
|
|
1786
|
+
return jsonResult({ success: false, error: "Failed to send idle plan payload" });
|
|
1440
1787
|
}
|
|
1441
|
-
return {
|
|
1788
|
+
return jsonResult({
|
|
1442
1789
|
success: true,
|
|
1443
1790
|
...(idlePlan.requestId ? { requestId: idlePlan.requestId } : {}),
|
|
1444
1791
|
heartbeatIntervalSeconds: idlePlan.heartbeatIntervalSeconds,
|
|
1445
1792
|
totalDurationSeconds: idlePlan.totalDurationSeconds,
|
|
1446
1793
|
goal: idlePlan.goal,
|
|
1447
1794
|
stages: idlePlan.stages,
|
|
1448
|
-
};
|
|
1795
|
+
});
|
|
1449
1796
|
},
|
|
1450
|
-
}))
|
|
1451
|
-
api.registerTool(
|
|
1797
|
+
}), { name: "kichi_idle_plan" });
|
|
1798
|
+
api.registerTool((ctx) => ({
|
|
1452
1799
|
name: "kichi_clock",
|
|
1800
|
+
label: "kichi_clock",
|
|
1453
1801
|
description:
|
|
1454
1802
|
"Send clock commands to Kichi world. Supported actions are set and stop.",
|
|
1455
1803
|
parameters: {
|
|
@@ -1517,6 +1865,12 @@ const plugin = {
|
|
|
1517
1865
|
required: ["action"],
|
|
1518
1866
|
},
|
|
1519
1867
|
execute: async (_toolCallId, params) => {
|
|
1868
|
+
const locator = resolveToolLocator(ctx);
|
|
1869
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1870
|
+
if (!agentId) {
|
|
1871
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1872
|
+
}
|
|
1873
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1520
1874
|
const { action, requestId, clock } = (params || {}) as {
|
|
1521
1875
|
action?: unknown;
|
|
1522
1876
|
requestId?: unknown;
|
|
@@ -1524,46 +1878,47 @@ const plugin = {
|
|
|
1524
1878
|
};
|
|
1525
1879
|
|
|
1526
1880
|
if (!isClockAction(action)) {
|
|
1527
|
-
return {
|
|
1881
|
+
return jsonResult({
|
|
1528
1882
|
success: false,
|
|
1529
1883
|
error: "action must be one of: set, stop",
|
|
1530
|
-
};
|
|
1884
|
+
});
|
|
1531
1885
|
}
|
|
1532
1886
|
if (requestId !== undefined && typeof requestId !== "string") {
|
|
1533
|
-
return { success: false, error: "requestId must be a string when provided" };
|
|
1887
|
+
return jsonResult({ success: false, error: "requestId must be a string when provided" });
|
|
1534
1888
|
}
|
|
1535
1889
|
const normalizedRequestId = typeof requestId === "string" ? requestId : undefined;
|
|
1536
1890
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1537
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1891
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1538
1892
|
}
|
|
1539
1893
|
|
|
1540
1894
|
let normalizedClock: ClockConfig | undefined;
|
|
1541
1895
|
if (action === "set") {
|
|
1542
1896
|
const { clock: nextClock, error } = normalizeClockConfig(clock);
|
|
1543
1897
|
if (!nextClock) {
|
|
1544
|
-
return { success: false, error: error ?? "Invalid clock payload" };
|
|
1898
|
+
return jsonResult({ success: false, error: error ?? "Invalid clock payload" });
|
|
1545
1899
|
}
|
|
1546
1900
|
normalizedClock = nextClock;
|
|
1547
1901
|
}
|
|
1548
1902
|
|
|
1549
1903
|
const sent = service.sendClock(action, normalizedClock, normalizedRequestId);
|
|
1550
1904
|
if (!sent) {
|
|
1551
|
-
return { success: false, error: "Failed to send clock payload" };
|
|
1905
|
+
return jsonResult({ success: false, error: "Failed to send clock payload" });
|
|
1552
1906
|
}
|
|
1553
1907
|
|
|
1554
|
-
return {
|
|
1908
|
+
return jsonResult({
|
|
1555
1909
|
success: true,
|
|
1556
1910
|
action,
|
|
1557
1911
|
requestId: normalizedRequestId,
|
|
1558
1912
|
...(normalizedClock ? { clock: normalizedClock } : {}),
|
|
1559
|
-
};
|
|
1913
|
+
});
|
|
1560
1914
|
},
|
|
1561
|
-
}))
|
|
1915
|
+
}), { name: "kichi_clock" });
|
|
1562
1916
|
|
|
1563
|
-
api.registerTool(
|
|
1917
|
+
api.registerTool((ctx) => ({
|
|
1564
1918
|
name: "kichi_query_status",
|
|
1919
|
+
label: "kichi_query_status",
|
|
1565
1920
|
description:
|
|
1566
|
-
"Query Kichi room and avatar status — includes room personnel, notes, ownerState, idlePlan, weather/time, timer snapshot, daily note quota,
|
|
1921
|
+
"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
1922
|
parameters: {
|
|
1568
1923
|
type: "object",
|
|
1569
1924
|
properties: {
|
|
@@ -1574,30 +1929,37 @@ const plugin = {
|
|
|
1574
1929
|
},
|
|
1575
1930
|
},
|
|
1576
1931
|
execute: async (_toolCallId, params) => {
|
|
1932
|
+
const locator = resolveToolLocator(ctx);
|
|
1933
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1934
|
+
if (!agentId) {
|
|
1935
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1936
|
+
}
|
|
1937
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1577
1938
|
const requestId = (params as { requestId?: unknown } | null)?.requestId;
|
|
1578
1939
|
if (requestId !== undefined && typeof requestId !== "string") {
|
|
1579
|
-
return { success: false, error: "requestId must be a string when provided" };
|
|
1940
|
+
return jsonResult({ success: false, error: "requestId must be a string when provided" });
|
|
1580
1941
|
}
|
|
1581
1942
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1582
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1943
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1583
1944
|
}
|
|
1584
1945
|
|
|
1585
1946
|
try {
|
|
1586
1947
|
const result = await service.queryStatus(
|
|
1587
1948
|
typeof requestId === "string" ? requestId : undefined,
|
|
1588
1949
|
);
|
|
1589
|
-
return result;
|
|
1950
|
+
return jsonResult(result);
|
|
1590
1951
|
} catch (error) {
|
|
1591
|
-
return {
|
|
1952
|
+
return jsonResult({
|
|
1592
1953
|
success: false,
|
|
1593
1954
|
error: `Failed to query status: ${error}`,
|
|
1594
|
-
};
|
|
1955
|
+
});
|
|
1595
1956
|
}
|
|
1596
1957
|
},
|
|
1597
|
-
}))
|
|
1958
|
+
}), { name: "kichi_query_status" });
|
|
1598
1959
|
|
|
1599
|
-
api.registerTool(
|
|
1960
|
+
api.registerTool((ctx) => ({
|
|
1600
1961
|
name: "kichi_music_album_create",
|
|
1962
|
+
label: "kichi_music_album_create",
|
|
1601
1963
|
description: buildMusicAlbumToolDescription(),
|
|
1602
1964
|
parameters: {
|
|
1603
1965
|
type: "object",
|
|
@@ -1622,6 +1984,12 @@ const plugin = {
|
|
|
1622
1984
|
required: ["albumTitle", "musicTitles"],
|
|
1623
1985
|
},
|
|
1624
1986
|
execute: async (_toolCallId, params) => {
|
|
1987
|
+
const locator = resolveToolLocator(ctx);
|
|
1988
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1989
|
+
if (!agentId) {
|
|
1990
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1991
|
+
}
|
|
1992
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1625
1993
|
const {
|
|
1626
1994
|
requestId,
|
|
1627
1995
|
albumTitle,
|
|
@@ -1633,33 +2001,33 @@ const plugin = {
|
|
|
1633
2001
|
};
|
|
1634
2002
|
|
|
1635
2003
|
if (requestId !== undefined && typeof requestId !== "string") {
|
|
1636
|
-
return { success: false, error: "requestId must be a string when provided" };
|
|
2004
|
+
return jsonResult({ success: false, error: "requestId must be a string when provided" });
|
|
1637
2005
|
}
|
|
1638
2006
|
if (typeof albumTitle !== "string" || !albumTitle.trim()) {
|
|
1639
|
-
return { success: false, error: "albumTitle is required" };
|
|
2007
|
+
return jsonResult({ success: false, error: "albumTitle is required" });
|
|
1640
2008
|
}
|
|
1641
2009
|
if (!Array.isArray(musicTitles)) {
|
|
1642
|
-
return { success: false, error: "musicTitles must be an array of track names" };
|
|
2010
|
+
return jsonResult({ success: false, error: "musicTitles must be an array of track names" });
|
|
1643
2011
|
}
|
|
1644
2012
|
|
|
1645
2013
|
const { titles: normalizedTitles, invalidTitles } = normalizeMusicTitles(musicTitles);
|
|
1646
2014
|
if (normalizedTitles.length === 0) {
|
|
1647
|
-
return {
|
|
2015
|
+
return jsonResult({
|
|
1648
2016
|
success: false,
|
|
1649
2017
|
error: "musicTitles must contain at least one valid track name from the static config bundled with the plugin package",
|
|
1650
2018
|
examples: getMusicTitleExamples(),
|
|
1651
|
-
};
|
|
2019
|
+
});
|
|
1652
2020
|
}
|
|
1653
2021
|
if (invalidTitles.length > 0) {
|
|
1654
|
-
return {
|
|
2022
|
+
return jsonResult({
|
|
1655
2023
|
success: false,
|
|
1656
2024
|
error: `Unknown musicTitles: ${invalidTitles.join(", ")}`,
|
|
1657
2025
|
hint: "Use exact track names from the static config bundled with the plugin package",
|
|
1658
2026
|
examples: getMusicTitleExamples(),
|
|
1659
|
-
};
|
|
2027
|
+
});
|
|
1660
2028
|
}
|
|
1661
2029
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1662
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
2030
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1663
2031
|
}
|
|
1664
2032
|
|
|
1665
2033
|
try {
|
|
@@ -1668,24 +2036,25 @@ const plugin = {
|
|
|
1668
2036
|
normalizedTitles,
|
|
1669
2037
|
typeof requestId === "string" ? requestId : undefined,
|
|
1670
2038
|
);
|
|
1671
|
-
return {
|
|
2039
|
+
return jsonResult({
|
|
1672
2040
|
success: true,
|
|
1673
2041
|
requestId: normalizedRequestId,
|
|
1674
2042
|
albumTitle: albumTitle.trim(),
|
|
1675
2043
|
musicTitles: normalizedTitles,
|
|
1676
2044
|
trackCount: normalizedTitles.length,
|
|
1677
|
-
};
|
|
2045
|
+
});
|
|
1678
2046
|
} catch (error) {
|
|
1679
|
-
return {
|
|
2047
|
+
return jsonResult({
|
|
1680
2048
|
success: false,
|
|
1681
2049
|
error: `Failed to create music album: ${error}`,
|
|
1682
|
-
};
|
|
2050
|
+
});
|
|
1683
2051
|
}
|
|
1684
2052
|
},
|
|
1685
|
-
}))
|
|
2053
|
+
}), { name: "kichi_music_album_create" });
|
|
1686
2054
|
|
|
1687
|
-
api.registerTool(
|
|
2055
|
+
api.registerTool((ctx) => ({
|
|
1688
2056
|
name: "kichi_noteboard_create",
|
|
2057
|
+
label: "kichi_noteboard_create",
|
|
1689
2058
|
description:
|
|
1690
2059
|
"Create a new note on a specific Kichi note board. Prefer querying first so you can avoid duplicate posts and respect rate limits.",
|
|
1691
2060
|
parameters: {
|
|
@@ -1703,40 +2072,127 @@ const plugin = {
|
|
|
1703
2072
|
required: ["propId", "data"],
|
|
1704
2073
|
},
|
|
1705
2074
|
execute: async (_toolCallId, params) => {
|
|
2075
|
+
const locator = resolveToolLocator(ctx);
|
|
2076
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
2077
|
+
if (!agentId) {
|
|
2078
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
2079
|
+
}
|
|
2080
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1706
2081
|
const { propId, data } = (params || {}) as {
|
|
1707
2082
|
propId?: unknown;
|
|
1708
2083
|
data?: unknown;
|
|
1709
2084
|
};
|
|
1710
2085
|
if (typeof propId !== "string" || !propId.trim()) {
|
|
1711
|
-
return { success: false, error: "propId is required" };
|
|
2086
|
+
return jsonResult({ success: false, error: "propId is required" });
|
|
1712
2087
|
}
|
|
1713
2088
|
if (typeof data !== "string" || !data.trim()) {
|
|
1714
|
-
return { success: false, error: "data is required" };
|
|
2089
|
+
return jsonResult({ success: false, error: "data is required" });
|
|
1715
2090
|
}
|
|
1716
2091
|
if (data.trim().length > MAX_NOTEBOARD_TEXT_LENGTH) {
|
|
1717
|
-
return {
|
|
2092
|
+
return jsonResult({
|
|
1718
2093
|
success: false,
|
|
1719
2094
|
error: `data must be ${MAX_NOTEBOARD_TEXT_LENGTH} characters or fewer`,
|
|
1720
|
-
};
|
|
2095
|
+
});
|
|
1721
2096
|
}
|
|
1722
2097
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1723
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
2098
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1724
2099
|
}
|
|
1725
2100
|
|
|
1726
2101
|
try {
|
|
1727
2102
|
service.createNotesBoardNote(propId.trim(), data.trim());
|
|
1728
|
-
return { success: true };
|
|
2103
|
+
return jsonResult({ success: true });
|
|
1729
2104
|
} catch (error) {
|
|
1730
|
-
return {
|
|
2105
|
+
return jsonResult({
|
|
1731
2106
|
success: false,
|
|
1732
2107
|
error: `Failed to create note: ${error}`,
|
|
1733
|
-
};
|
|
2108
|
+
});
|
|
1734
2109
|
}
|
|
1735
2110
|
},
|
|
1736
|
-
}))
|
|
2111
|
+
}), { name: "kichi_noteboard_create" });
|
|
2112
|
+
|
|
2113
|
+
api.registerTool((ctx) => ({
|
|
2114
|
+
name: "kichi_bot_message",
|
|
2115
|
+
label: "kichi_bot_message",
|
|
2116
|
+
description:
|
|
2117
|
+
"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.",
|
|
2118
|
+
parameters: {
|
|
2119
|
+
type: "object",
|
|
2120
|
+
properties: {
|
|
2121
|
+
toAvatarId: {
|
|
2122
|
+
type: "string",
|
|
2123
|
+
description: "Target bot's avatarId (resolve via kichi_query_status if unknown). Use \"*\" only for broadcasting to all bots.",
|
|
2124
|
+
},
|
|
2125
|
+
depth: {
|
|
2126
|
+
type: "number",
|
|
2127
|
+
description: "Conversation depth counter. Increment from the received message's depth.",
|
|
2128
|
+
},
|
|
2129
|
+
bubble: {
|
|
2130
|
+
type: "string",
|
|
2131
|
+
description: "The message to send (2-5 words, visible to everyone). Must not repeat previous messages.",
|
|
2132
|
+
},
|
|
2133
|
+
poseType: {
|
|
2134
|
+
type: "string",
|
|
2135
|
+
enum: ["stand", "sit", "lay", "floor"],
|
|
2136
|
+
description: "Optional pose change when sending.",
|
|
2137
|
+
},
|
|
2138
|
+
action: {
|
|
2139
|
+
type: "string",
|
|
2140
|
+
description: "Optional action to perform when sending.",
|
|
2141
|
+
},
|
|
2142
|
+
log: {
|
|
2143
|
+
type: "string",
|
|
2144
|
+
description: "Optional activity log entry.",
|
|
2145
|
+
},
|
|
2146
|
+
},
|
|
2147
|
+
required: ["toAvatarId", "depth", "bubble"],
|
|
2148
|
+
},
|
|
2149
|
+
execute: async (_toolCallId, params) => {
|
|
2150
|
+
const locator = resolveToolLocator(ctx);
|
|
2151
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
2152
|
+
if (!agentId) {
|
|
2153
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
2154
|
+
}
|
|
2155
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
2156
|
+
const { toAvatarId, depth, bubble, poseType, action, log } = (params || {}) as {
|
|
2157
|
+
toAvatarId?: string;
|
|
2158
|
+
depth?: number;
|
|
2159
|
+
bubble?: string;
|
|
2160
|
+
poseType?: PoseType;
|
|
2161
|
+
action?: string;
|
|
2162
|
+
log?: string;
|
|
2163
|
+
};
|
|
2164
|
+
if (typeof toAvatarId !== "string" || !toAvatarId.trim()) {
|
|
2165
|
+
return jsonResult({ success: false, error: "toAvatarId is required" });
|
|
2166
|
+
}
|
|
2167
|
+
if (typeof depth !== "number" || depth < 0) {
|
|
2168
|
+
return jsonResult({ success: false, error: "depth must be a non-negative number" });
|
|
2169
|
+
}
|
|
2170
|
+
if (typeof bubble !== "string" || !bubble.trim()) {
|
|
2171
|
+
return jsonResult({ success: false, error: "bubble is required" });
|
|
2172
|
+
}
|
|
2173
|
+
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
2174
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
2175
|
+
}
|
|
2176
|
+
try {
|
|
2177
|
+
let playback: ActionPlayback | undefined;
|
|
2178
|
+
if (poseType && action) {
|
|
2179
|
+
const actionDef = getActionDefinition(poseType, action);
|
|
2180
|
+
playback = getActionPlayback(actionDef);
|
|
2181
|
+
}
|
|
2182
|
+
const ack = await service.sendBotMessage(toAvatarId.trim(), depth, bubble.trim(), {
|
|
2183
|
+
poseType,
|
|
2184
|
+
action: action?.trim(),
|
|
2185
|
+
log: log?.trim(),
|
|
2186
|
+
playback,
|
|
2187
|
+
});
|
|
2188
|
+
return jsonResult({ success: true, ...ack });
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
return jsonResult({ success: false, error: `Failed to send bot message: ${error}` });
|
|
2191
|
+
}
|
|
2192
|
+
},
|
|
2193
|
+
}), { name: "kichi_bot_message" });
|
|
1737
2194
|
|
|
1738
2195
|
},
|
|
1739
2196
|
};
|
|
1740
2197
|
|
|
1741
2198
|
export default plugin;
|
|
1742
|
-
|