@kernel.chat/kbot 3.93.0 → 3.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/coordination-engine.d.ts +127 -0
- package/dist/tools/coordination-engine.js +543 -0
- package/dist/tools/foundation-engines.d.ts +111 -0
- package/dist/tools/foundation-engines.js +520 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/research-engine.d.ts +58 -0
- package/dist/tools/research-engine.js +550 -0
- package/dist/tools/stream-renderer.js +289 -144
- package/package.json +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* coordination-engine.ts — Master engine that manages all other engines.
|
|
3
|
+
*
|
|
4
|
+
* The Coordination Engine prevents engines from stepping on each other by:
|
|
5
|
+
* 1. Priority-based speech queue — only the most important message shows
|
|
6
|
+
* 2. Mood stack — highest priority mood wins
|
|
7
|
+
* 3. Blackboard — engines communicate through key/value store with TTLs
|
|
8
|
+
* 4. Engine status tracking — self-reported health from every engine
|
|
9
|
+
* 5. Resource budgets — frame timing, speech slot caps
|
|
10
|
+
*
|
|
11
|
+
* Priority levels (by convention):
|
|
12
|
+
* 95: System alerts (stream offline, engine crash)
|
|
13
|
+
* 90: Follower/subscriber celebrations
|
|
14
|
+
* 80: Chat responses (someone talked to kbot)
|
|
15
|
+
* 70: Evolution discoveries (new technique applied)
|
|
16
|
+
* 60: Brain tool execution results
|
|
17
|
+
* 50: Narrative observations
|
|
18
|
+
* 40: Exploration narration (walking, examining)
|
|
19
|
+
* 30: Autonomous idle behavior
|
|
20
|
+
* 20: Audio atmosphere descriptions
|
|
21
|
+
* 10: Inner thoughts
|
|
22
|
+
*
|
|
23
|
+
* Integration: imported by stream-renderer.ts, wired into the frame loop.
|
|
24
|
+
* Does NOT import or modify any other engine file.
|
|
25
|
+
*/
|
|
26
|
+
export interface CoordinationEngine {
|
|
27
|
+
speechQueue: SpeechItem[];
|
|
28
|
+
currentSpeech: SpeechItem | null;
|
|
29
|
+
currentSpeechExpiry: number;
|
|
30
|
+
moodStack: MoodRequest[];
|
|
31
|
+
engineStatus: Map<string, EngineStatus>;
|
|
32
|
+
blackboard: Map<string, BlackboardMessage>;
|
|
33
|
+
frameCount: number;
|
|
34
|
+
resourceBudget: ResourceBudget;
|
|
35
|
+
}
|
|
36
|
+
export interface SpeechItem {
|
|
37
|
+
text: string;
|
|
38
|
+
mood: string;
|
|
39
|
+
priority: number;
|
|
40
|
+
duration: number;
|
|
41
|
+
source: string;
|
|
42
|
+
timestamp: number;
|
|
43
|
+
}
|
|
44
|
+
export interface MoodRequest {
|
|
45
|
+
mood: string;
|
|
46
|
+
priority: number;
|
|
47
|
+
source: string;
|
|
48
|
+
expiresAt: number;
|
|
49
|
+
}
|
|
50
|
+
export interface EngineStatus {
|
|
51
|
+
name: string;
|
|
52
|
+
active: boolean;
|
|
53
|
+
lastTick: number;
|
|
54
|
+
ticksPerSecond: number;
|
|
55
|
+
errors: number;
|
|
56
|
+
outputCount: number;
|
|
57
|
+
}
|
|
58
|
+
export interface BlackboardMessage {
|
|
59
|
+
key: string;
|
|
60
|
+
value: unknown;
|
|
61
|
+
source: string;
|
|
62
|
+
timestamp: number;
|
|
63
|
+
ttl: number;
|
|
64
|
+
}
|
|
65
|
+
export interface ResourceBudget {
|
|
66
|
+
frameBudgetMs: number;
|
|
67
|
+
speechSlots: number;
|
|
68
|
+
activeEngines: number;
|
|
69
|
+
}
|
|
70
|
+
export interface CoordinationOutput {
|
|
71
|
+
speech: string | null;
|
|
72
|
+
mood: string;
|
|
73
|
+
shouldWalk: boolean;
|
|
74
|
+
walkTarget: number | null;
|
|
75
|
+
effects: string[];
|
|
76
|
+
announcements: string[];
|
|
77
|
+
}
|
|
78
|
+
export declare function initCoordination(): CoordinationEngine;
|
|
79
|
+
/**
|
|
80
|
+
* Add a speech item to the priority queue.
|
|
81
|
+
* Queue is capped at MAX_SPEECH_QUEUE — lowest priority items are evicted.
|
|
82
|
+
*/
|
|
83
|
+
export declare function queueSpeech(coord: CoordinationEngine, text: string, mood: string, priority: number, duration: number, source: string): void;
|
|
84
|
+
/**
|
|
85
|
+
* Dequeue the highest priority speech when the current one expires.
|
|
86
|
+
* Returns the speech to display or null if nothing is ready.
|
|
87
|
+
*/
|
|
88
|
+
export declare function tickSpeech(coord: CoordinationEngine, frame: number): {
|
|
89
|
+
text: string;
|
|
90
|
+
mood: string;
|
|
91
|
+
} | null;
|
|
92
|
+
/**
|
|
93
|
+
* Request a mood. Highest priority mood wins on resolve.
|
|
94
|
+
* Duration is in frames.
|
|
95
|
+
*/
|
|
96
|
+
export declare function requestMood(coord: CoordinationEngine, mood: string, priority: number, source: string, duration: number): void;
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the current winning mood. Expires stale entries.
|
|
99
|
+
* Returns the mood string of the highest-priority active request,
|
|
100
|
+
* or DEFAULT_MOOD if the stack is empty.
|
|
101
|
+
*/
|
|
102
|
+
export declare function resolveMood(coord: CoordinationEngine, frame: number): string;
|
|
103
|
+
/**
|
|
104
|
+
* Post a message to the blackboard. Engines communicate through here.
|
|
105
|
+
* TTL is in frames. Overwrites existing key from any source.
|
|
106
|
+
*/
|
|
107
|
+
export declare function postToBlackboard(coord: CoordinationEngine, key: string, value: unknown, source: string, ttl: number): void;
|
|
108
|
+
/**
|
|
109
|
+
* Read the latest value for a key from the blackboard.
|
|
110
|
+
* Returns undefined if the key does not exist or has expired.
|
|
111
|
+
*/
|
|
112
|
+
export declare function readBlackboard(coord: CoordinationEngine, key: string): unknown;
|
|
113
|
+
/**
|
|
114
|
+
* Engines self-report their health status each tick.
|
|
115
|
+
*/
|
|
116
|
+
export declare function reportEngineStatus(coord: CoordinationEngine, name: string, active: boolean, errors: number): void;
|
|
117
|
+
/**
|
|
118
|
+
* Master coordination tick. Called once per frame.
|
|
119
|
+
* Returns what should be displayed/acted on this frame.
|
|
120
|
+
*/
|
|
121
|
+
export declare function tickCoordination(coord: CoordinationEngine, frame: number): CoordinationOutput;
|
|
122
|
+
export declare function serializeCoordination(coord: CoordinationEngine): string;
|
|
123
|
+
export declare function deserializeCoordination(json: string): CoordinationEngine;
|
|
124
|
+
/** Reset the singleton (for testing or re-init) */
|
|
125
|
+
export declare function resetCoordination(): void;
|
|
126
|
+
export declare function registerCoordinationEngineTools(): void;
|
|
127
|
+
//# sourceMappingURL=coordination-engine.d.ts.map
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* coordination-engine.ts — Master engine that manages all other engines.
|
|
3
|
+
*
|
|
4
|
+
* The Coordination Engine prevents engines from stepping on each other by:
|
|
5
|
+
* 1. Priority-based speech queue — only the most important message shows
|
|
6
|
+
* 2. Mood stack — highest priority mood wins
|
|
7
|
+
* 3. Blackboard — engines communicate through key/value store with TTLs
|
|
8
|
+
* 4. Engine status tracking — self-reported health from every engine
|
|
9
|
+
* 5. Resource budgets — frame timing, speech slot caps
|
|
10
|
+
*
|
|
11
|
+
* Priority levels (by convention):
|
|
12
|
+
* 95: System alerts (stream offline, engine crash)
|
|
13
|
+
* 90: Follower/subscriber celebrations
|
|
14
|
+
* 80: Chat responses (someone talked to kbot)
|
|
15
|
+
* 70: Evolution discoveries (new technique applied)
|
|
16
|
+
* 60: Brain tool execution results
|
|
17
|
+
* 50: Narrative observations
|
|
18
|
+
* 40: Exploration narration (walking, examining)
|
|
19
|
+
* 30: Autonomous idle behavior
|
|
20
|
+
* 20: Audio atmosphere descriptions
|
|
21
|
+
* 10: Inner thoughts
|
|
22
|
+
*
|
|
23
|
+
* Integration: imported by stream-renderer.ts, wired into the frame loop.
|
|
24
|
+
* Does NOT import or modify any other engine file.
|
|
25
|
+
*/
|
|
26
|
+
import { registerTool } from './index.js';
|
|
27
|
+
// ─── Constants ───────────────────────────────────────────────────
|
|
28
|
+
const MAX_SPEECH_QUEUE = 10;
|
|
29
|
+
const DEFAULT_SPEECH_DURATION = 360; // ~60 seconds at 6 fps
|
|
30
|
+
const DEFAULT_MOOD = 'calm';
|
|
31
|
+
const DEFAULT_FRAME_BUDGET_MS = 150;
|
|
32
|
+
const DEFAULT_SPEECH_SLOTS = 10;
|
|
33
|
+
const DEFAULT_ACTIVE_ENGINES = 10;
|
|
34
|
+
// ─── Initialization ──────────────────────────────────────────────
|
|
35
|
+
export function initCoordination() {
|
|
36
|
+
return {
|
|
37
|
+
speechQueue: [],
|
|
38
|
+
currentSpeech: null,
|
|
39
|
+
currentSpeechExpiry: 0,
|
|
40
|
+
moodStack: [],
|
|
41
|
+
engineStatus: new Map(),
|
|
42
|
+
blackboard: new Map(),
|
|
43
|
+
frameCount: 0,
|
|
44
|
+
resourceBudget: {
|
|
45
|
+
frameBudgetMs: DEFAULT_FRAME_BUDGET_MS,
|
|
46
|
+
speechSlots: DEFAULT_SPEECH_SLOTS,
|
|
47
|
+
activeEngines: DEFAULT_ACTIVE_ENGINES,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
// ─── Speech Queue ────────────────────────────────────────────────
|
|
52
|
+
/**
|
|
53
|
+
* Add a speech item to the priority queue.
|
|
54
|
+
* Queue is capped at MAX_SPEECH_QUEUE — lowest priority items are evicted.
|
|
55
|
+
*/
|
|
56
|
+
export function queueSpeech(coord, text, mood, priority, duration, source) {
|
|
57
|
+
const item = {
|
|
58
|
+
text,
|
|
59
|
+
mood,
|
|
60
|
+
priority: Math.max(0, Math.min(100, priority)),
|
|
61
|
+
duration: duration > 0 ? duration : DEFAULT_SPEECH_DURATION,
|
|
62
|
+
source,
|
|
63
|
+
timestamp: Date.now(),
|
|
64
|
+
};
|
|
65
|
+
coord.speechQueue.push(item);
|
|
66
|
+
// Sort descending by priority, then by timestamp (FIFO within same priority)
|
|
67
|
+
coord.speechQueue.sort((a, b) => {
|
|
68
|
+
if (b.priority !== a.priority)
|
|
69
|
+
return b.priority - a.priority;
|
|
70
|
+
return a.timestamp - b.timestamp;
|
|
71
|
+
});
|
|
72
|
+
// Cap the queue — evict lowest priority items
|
|
73
|
+
if (coord.speechQueue.length > MAX_SPEECH_QUEUE) {
|
|
74
|
+
coord.speechQueue.length = MAX_SPEECH_QUEUE;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Dequeue the highest priority speech when the current one expires.
|
|
79
|
+
* Returns the speech to display or null if nothing is ready.
|
|
80
|
+
*/
|
|
81
|
+
export function tickSpeech(coord, frame) {
|
|
82
|
+
// Check if current speech has expired
|
|
83
|
+
if (coord.currentSpeech && frame < coord.currentSpeechExpiry) {
|
|
84
|
+
return { text: coord.currentSpeech.text, mood: coord.currentSpeech.mood };
|
|
85
|
+
}
|
|
86
|
+
// Current speech expired — try to dequeue next
|
|
87
|
+
coord.currentSpeech = null;
|
|
88
|
+
if (coord.speechQueue.length === 0) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// Take highest priority item (already sorted)
|
|
92
|
+
const next = coord.speechQueue.shift();
|
|
93
|
+
coord.currentSpeech = next;
|
|
94
|
+
coord.currentSpeechExpiry = frame + next.duration;
|
|
95
|
+
return { text: next.text, mood: next.mood };
|
|
96
|
+
}
|
|
97
|
+
// ─── Mood Stack ──────────────────────────────────────────────────
|
|
98
|
+
/**
|
|
99
|
+
* Request a mood. Highest priority mood wins on resolve.
|
|
100
|
+
* Duration is in frames.
|
|
101
|
+
*/
|
|
102
|
+
export function requestMood(coord, mood, priority, source, duration) {
|
|
103
|
+
const expiresAt = coord.frameCount + Math.max(1, duration);
|
|
104
|
+
// Replace existing request from the same source
|
|
105
|
+
const existingIdx = coord.moodStack.findIndex(m => m.source === source);
|
|
106
|
+
if (existingIdx >= 0) {
|
|
107
|
+
coord.moodStack[existingIdx] = { mood, priority, source, expiresAt };
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
coord.moodStack.push({ mood, priority, source, expiresAt });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the current winning mood. Expires stale entries.
|
|
115
|
+
* Returns the mood string of the highest-priority active request,
|
|
116
|
+
* or DEFAULT_MOOD if the stack is empty.
|
|
117
|
+
*/
|
|
118
|
+
export function resolveMood(coord, frame) {
|
|
119
|
+
// Purge expired entries
|
|
120
|
+
coord.moodStack = coord.moodStack.filter(m => m.expiresAt > frame);
|
|
121
|
+
if (coord.moodStack.length === 0)
|
|
122
|
+
return DEFAULT_MOOD;
|
|
123
|
+
// Highest priority wins; ties broken by most recent expiresAt
|
|
124
|
+
let winner = coord.moodStack[0];
|
|
125
|
+
for (let i = 1; i < coord.moodStack.length; i++) {
|
|
126
|
+
const m = coord.moodStack[i];
|
|
127
|
+
if (m.priority > winner.priority) {
|
|
128
|
+
winner = m;
|
|
129
|
+
}
|
|
130
|
+
else if (m.priority === winner.priority && m.expiresAt > winner.expiresAt) {
|
|
131
|
+
winner = m;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return winner.mood;
|
|
135
|
+
}
|
|
136
|
+
// ─── Blackboard ──────────────────────────────────────────────────
|
|
137
|
+
/**
|
|
138
|
+
* Post a message to the blackboard. Engines communicate through here.
|
|
139
|
+
* TTL is in frames. Overwrites existing key from any source.
|
|
140
|
+
*/
|
|
141
|
+
export function postToBlackboard(coord, key, value, source, ttl) {
|
|
142
|
+
coord.blackboard.set(key, {
|
|
143
|
+
key,
|
|
144
|
+
value,
|
|
145
|
+
source,
|
|
146
|
+
timestamp: Date.now(),
|
|
147
|
+
ttl: Math.max(1, ttl),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Read the latest value for a key from the blackboard.
|
|
152
|
+
* Returns undefined if the key does not exist or has expired.
|
|
153
|
+
*/
|
|
154
|
+
export function readBlackboard(coord, key) {
|
|
155
|
+
const msg = coord.blackboard.get(key);
|
|
156
|
+
if (!msg)
|
|
157
|
+
return undefined;
|
|
158
|
+
return msg.value;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Expire stale blackboard entries. Called during tickCoordination.
|
|
162
|
+
*/
|
|
163
|
+
function expireBlackboard(coord, frame) {
|
|
164
|
+
const toDelete = [];
|
|
165
|
+
for (const [key, msg] of coord.blackboard) {
|
|
166
|
+
// TTL is relative to the frame the message was posted.
|
|
167
|
+
// We check against the posting timestamp converted to frames (rough),
|
|
168
|
+
// but a simpler approach: decrement TTL each tick.
|
|
169
|
+
// Since we call this every tick, just decrement.
|
|
170
|
+
msg.ttl--;
|
|
171
|
+
if (msg.ttl <= 0) {
|
|
172
|
+
toDelete.push(key);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const key of toDelete) {
|
|
176
|
+
coord.blackboard.delete(key);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// ─── Engine Status ───────────────────────────────────────────────
|
|
180
|
+
/**
|
|
181
|
+
* Engines self-report their health status each tick.
|
|
182
|
+
*/
|
|
183
|
+
export function reportEngineStatus(coord, name, active, errors) {
|
|
184
|
+
const existing = coord.engineStatus.get(name);
|
|
185
|
+
const now = Date.now();
|
|
186
|
+
if (existing) {
|
|
187
|
+
// Calculate ticks per second
|
|
188
|
+
const elapsed = now - existing.lastTick;
|
|
189
|
+
existing.ticksPerSecond = elapsed > 0 ? 1000 / elapsed : 0;
|
|
190
|
+
existing.active = active;
|
|
191
|
+
existing.lastTick = now;
|
|
192
|
+
existing.errors = errors;
|
|
193
|
+
existing.outputCount++;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
coord.engineStatus.set(name, {
|
|
197
|
+
name,
|
|
198
|
+
active,
|
|
199
|
+
lastTick: now,
|
|
200
|
+
ticksPerSecond: 0,
|
|
201
|
+
errors,
|
|
202
|
+
outputCount: 1,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
// Generate system alerts for engine crashes (errors > 5)
|
|
206
|
+
if (errors > 5 && active) {
|
|
207
|
+
queueSpeech(coord, `Warning: ${name} engine reporting ${errors} errors.`, 'worried', 95, 180, // ~30 seconds at 6 fps
|
|
208
|
+
'coordination');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// ─── Master Tick ─────────────────────────────────────────────────
|
|
212
|
+
/**
|
|
213
|
+
* Master coordination tick. Called once per frame.
|
|
214
|
+
* Returns what should be displayed/acted on this frame.
|
|
215
|
+
*/
|
|
216
|
+
export function tickCoordination(coord, frame) {
|
|
217
|
+
coord.frameCount = frame;
|
|
218
|
+
// 1. Expire stale blackboard entries
|
|
219
|
+
expireBlackboard(coord, frame);
|
|
220
|
+
// 2. Resolve current mood
|
|
221
|
+
const mood = resolveMood(coord, frame);
|
|
222
|
+
// 3. Resolve current speech
|
|
223
|
+
const speechResult = tickSpeech(coord, frame);
|
|
224
|
+
// 4. Check blackboard for walk directives
|
|
225
|
+
const walkTarget = readBlackboard(coord, 'walk_target');
|
|
226
|
+
const shouldWalk = walkTarget != null && walkTarget !== undefined;
|
|
227
|
+
// 5. Collect effects from blackboard
|
|
228
|
+
const effects = [];
|
|
229
|
+
const effectMsg = readBlackboard(coord, 'effects');
|
|
230
|
+
if (Array.isArray(effectMsg)) {
|
|
231
|
+
effects.push(...effectMsg);
|
|
232
|
+
}
|
|
233
|
+
else if (typeof effectMsg === 'string') {
|
|
234
|
+
effects.push(effectMsg);
|
|
235
|
+
}
|
|
236
|
+
// 6. Collect announcements — high-priority messages that should be logged
|
|
237
|
+
const announcements = [];
|
|
238
|
+
const announcementMsg = readBlackboard(coord, 'announcements');
|
|
239
|
+
if (Array.isArray(announcementMsg)) {
|
|
240
|
+
announcements.push(...announcementMsg);
|
|
241
|
+
}
|
|
242
|
+
else if (typeof announcementMsg === 'string') {
|
|
243
|
+
announcements.push(announcementMsg);
|
|
244
|
+
}
|
|
245
|
+
// 7. Update resource budget based on active engine count
|
|
246
|
+
coord.resourceBudget.activeEngines = 0;
|
|
247
|
+
for (const [, status] of coord.engineStatus) {
|
|
248
|
+
if (status.active)
|
|
249
|
+
coord.resourceBudget.activeEngines++;
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
speech: speechResult?.text ?? null,
|
|
253
|
+
mood,
|
|
254
|
+
shouldWalk,
|
|
255
|
+
walkTarget: shouldWalk ? walkTarget : null,
|
|
256
|
+
effects,
|
|
257
|
+
announcements,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
export function serializeCoordination(coord) {
|
|
261
|
+
const state = {
|
|
262
|
+
speechQueue: coord.speechQueue,
|
|
263
|
+
currentSpeech: coord.currentSpeech,
|
|
264
|
+
currentSpeechExpiry: coord.currentSpeechExpiry,
|
|
265
|
+
moodStack: coord.moodStack,
|
|
266
|
+
engineStatus: Object.fromEntries(coord.engineStatus),
|
|
267
|
+
blackboard: Object.fromEntries(coord.blackboard),
|
|
268
|
+
frameCount: coord.frameCount,
|
|
269
|
+
resourceBudget: coord.resourceBudget,
|
|
270
|
+
};
|
|
271
|
+
return JSON.stringify(state, null, 2);
|
|
272
|
+
}
|
|
273
|
+
export function deserializeCoordination(json) {
|
|
274
|
+
const state = JSON.parse(json);
|
|
275
|
+
return {
|
|
276
|
+
speechQueue: state.speechQueue ?? [],
|
|
277
|
+
currentSpeech: state.currentSpeech ?? null,
|
|
278
|
+
currentSpeechExpiry: state.currentSpeechExpiry ?? 0,
|
|
279
|
+
moodStack: state.moodStack ?? [],
|
|
280
|
+
engineStatus: new Map(Object.entries(state.engineStatus ?? {})),
|
|
281
|
+
blackboard: new Map(Object.entries(state.blackboard ?? {})),
|
|
282
|
+
frameCount: state.frameCount ?? 0,
|
|
283
|
+
resourceBudget: state.resourceBudget ?? {
|
|
284
|
+
frameBudgetMs: DEFAULT_FRAME_BUDGET_MS,
|
|
285
|
+
speechSlots: DEFAULT_SPEECH_SLOTS,
|
|
286
|
+
activeEngines: DEFAULT_ACTIVE_ENGINES,
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
// ─── Tool Registration ───────────────────────────────────────────
|
|
291
|
+
// Module-level singleton so tools share state within a session
|
|
292
|
+
let _engine = null;
|
|
293
|
+
function getEngine() {
|
|
294
|
+
if (!_engine)
|
|
295
|
+
_engine = initCoordination();
|
|
296
|
+
return _engine;
|
|
297
|
+
}
|
|
298
|
+
/** Reset the singleton (for testing or re-init) */
|
|
299
|
+
export function resetCoordination() {
|
|
300
|
+
_engine = null;
|
|
301
|
+
}
|
|
302
|
+
export function registerCoordinationEngineTools() {
|
|
303
|
+
// ── coordination_status ──
|
|
304
|
+
registerTool({
|
|
305
|
+
name: 'coordination_status',
|
|
306
|
+
description: 'View the Coordination Engine status: speech queue, mood stack, engine health, blackboard contents, and resource budget. ' +
|
|
307
|
+
'The Coordination Engine is the master arbiter that prevents engines from stepping on each other. ' +
|
|
308
|
+
'Use "section" to view a specific section (speech, mood, engines, blackboard, budget). Omit for full overview.',
|
|
309
|
+
parameters: {
|
|
310
|
+
section: {
|
|
311
|
+
type: 'string',
|
|
312
|
+
description: 'Section to view: speech, mood, engines, blackboard, budget. Omit for full overview.',
|
|
313
|
+
required: false,
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
tier: 'free',
|
|
317
|
+
execute: async (args) => {
|
|
318
|
+
const coord = getEngine();
|
|
319
|
+
const section = args.section;
|
|
320
|
+
const lines = [];
|
|
321
|
+
// Speech section
|
|
322
|
+
if (!section || section === 'speech') {
|
|
323
|
+
lines.push('Speech Queue');
|
|
324
|
+
lines.push('════════════');
|
|
325
|
+
lines.push(`Current speech: ${coord.currentSpeech ? `"${coord.currentSpeech.text}" (from ${coord.currentSpeech.source}, priority ${coord.currentSpeech.priority})` : '(none)'}`);
|
|
326
|
+
lines.push(`Expires at frame: ${coord.currentSpeechExpiry}`);
|
|
327
|
+
lines.push(`Queue depth: ${coord.speechQueue.length}/${MAX_SPEECH_QUEUE}`);
|
|
328
|
+
if (coord.speechQueue.length > 0) {
|
|
329
|
+
lines.push('');
|
|
330
|
+
lines.push('Queued:');
|
|
331
|
+
for (const item of coord.speechQueue) {
|
|
332
|
+
lines.push(` [P${item.priority}] "${item.text.slice(0, 60)}${item.text.length > 60 ? '...' : ''}" (${item.source}, ${item.duration} frames)`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
lines.push('');
|
|
336
|
+
}
|
|
337
|
+
// Mood section
|
|
338
|
+
if (!section || section === 'mood') {
|
|
339
|
+
lines.push('Mood Stack');
|
|
340
|
+
lines.push('══════════');
|
|
341
|
+
const currentMood = resolveMood(coord, coord.frameCount);
|
|
342
|
+
lines.push(`Current mood: ${currentMood}`);
|
|
343
|
+
lines.push(`Active requests: ${coord.moodStack.length}`);
|
|
344
|
+
if (coord.moodStack.length > 0) {
|
|
345
|
+
for (const m of coord.moodStack) {
|
|
346
|
+
lines.push(` [P${m.priority}] ${m.mood} from ${m.source} (expires frame ${m.expiresAt})`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
lines.push('');
|
|
350
|
+
}
|
|
351
|
+
// Engine health section
|
|
352
|
+
if (!section || section === 'engines') {
|
|
353
|
+
lines.push('Engine Status');
|
|
354
|
+
lines.push('═════════════');
|
|
355
|
+
if (coord.engineStatus.size === 0) {
|
|
356
|
+
lines.push(' (no engines reporting)');
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
for (const [, status] of coord.engineStatus) {
|
|
360
|
+
const state = status.active ? 'ACTIVE' : 'IDLE';
|
|
361
|
+
const errStr = status.errors > 0 ? ` (${status.errors} errors)` : '';
|
|
362
|
+
lines.push(` ${status.name}: ${state} — ${status.ticksPerSecond.toFixed(1)} tps, ${status.outputCount} outputs${errStr}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
lines.push('');
|
|
366
|
+
}
|
|
367
|
+
// Blackboard section
|
|
368
|
+
if (!section || section === 'blackboard') {
|
|
369
|
+
lines.push('Blackboard');
|
|
370
|
+
lines.push('══════════');
|
|
371
|
+
if (coord.blackboard.size === 0) {
|
|
372
|
+
lines.push(' (empty)');
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
for (const [key, msg] of coord.blackboard) {
|
|
376
|
+
const val = typeof msg.value === 'string' ? msg.value : JSON.stringify(msg.value);
|
|
377
|
+
const truncVal = val.length > 80 ? val.slice(0, 80) + '...' : val;
|
|
378
|
+
lines.push(` ${key}: ${truncVal} (from ${msg.source}, TTL ${msg.ttl} frames)`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
lines.push('');
|
|
382
|
+
}
|
|
383
|
+
// Resource budget section
|
|
384
|
+
if (!section || section === 'budget') {
|
|
385
|
+
lines.push('Resource Budget');
|
|
386
|
+
lines.push('═══════════════');
|
|
387
|
+
lines.push(`Frame budget: ${coord.resourceBudget.frameBudgetMs}ms`);
|
|
388
|
+
lines.push(`Speech slots: ${coord.resourceBudget.speechSlots}`);
|
|
389
|
+
lines.push(`Active engines: ${coord.resourceBudget.activeEngines}`);
|
|
390
|
+
lines.push(`Current frame: ${coord.frameCount}`);
|
|
391
|
+
lines.push('');
|
|
392
|
+
}
|
|
393
|
+
return lines.join('\n');
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
// ── coordination_queue ──
|
|
397
|
+
registerTool({
|
|
398
|
+
name: 'coordination_queue',
|
|
399
|
+
description: 'Queue speech, request mood changes, post to the blackboard, or report engine status through the Coordination Engine. ' +
|
|
400
|
+
'Use "action" to specify what to do: queue_speech, request_mood, post_blackboard, report_status, tick. ' +
|
|
401
|
+
'The tick action advances the coordination engine by one frame and returns the CoordinationOutput.',
|
|
402
|
+
parameters: {
|
|
403
|
+
action: {
|
|
404
|
+
type: 'string',
|
|
405
|
+
description: 'Action: queue_speech, request_mood, post_blackboard, report_status, tick',
|
|
406
|
+
required: true,
|
|
407
|
+
},
|
|
408
|
+
text: {
|
|
409
|
+
type: 'string',
|
|
410
|
+
description: 'Speech text (for queue_speech)',
|
|
411
|
+
required: false,
|
|
412
|
+
},
|
|
413
|
+
mood: {
|
|
414
|
+
type: 'string',
|
|
415
|
+
description: 'Mood string (for queue_speech or request_mood)',
|
|
416
|
+
required: false,
|
|
417
|
+
},
|
|
418
|
+
priority: {
|
|
419
|
+
type: 'number',
|
|
420
|
+
description: 'Priority 0-100 (for queue_speech or request_mood). See priority level conventions in engine docs.',
|
|
421
|
+
required: false,
|
|
422
|
+
},
|
|
423
|
+
duration: {
|
|
424
|
+
type: 'number',
|
|
425
|
+
description: 'Duration in frames (for queue_speech or request_mood)',
|
|
426
|
+
required: false,
|
|
427
|
+
},
|
|
428
|
+
source: {
|
|
429
|
+
type: 'string',
|
|
430
|
+
description: 'Source engine name (for queue_speech, request_mood, post_blackboard, report_status)',
|
|
431
|
+
required: false,
|
|
432
|
+
},
|
|
433
|
+
key: {
|
|
434
|
+
type: 'string',
|
|
435
|
+
description: 'Blackboard key (for post_blackboard)',
|
|
436
|
+
required: false,
|
|
437
|
+
},
|
|
438
|
+
value: {
|
|
439
|
+
type: 'string',
|
|
440
|
+
description: 'Blackboard value as JSON string (for post_blackboard)',
|
|
441
|
+
required: false,
|
|
442
|
+
},
|
|
443
|
+
ttl: {
|
|
444
|
+
type: 'number',
|
|
445
|
+
description: 'TTL in frames for blackboard entry (for post_blackboard)',
|
|
446
|
+
required: false,
|
|
447
|
+
},
|
|
448
|
+
engine_name: {
|
|
449
|
+
type: 'string',
|
|
450
|
+
description: 'Engine name (for report_status)',
|
|
451
|
+
required: false,
|
|
452
|
+
},
|
|
453
|
+
active: {
|
|
454
|
+
type: 'boolean',
|
|
455
|
+
description: 'Whether engine is active (for report_status)',
|
|
456
|
+
required: false,
|
|
457
|
+
},
|
|
458
|
+
errors: {
|
|
459
|
+
type: 'number',
|
|
460
|
+
description: 'Error count (for report_status)',
|
|
461
|
+
required: false,
|
|
462
|
+
},
|
|
463
|
+
frame: {
|
|
464
|
+
type: 'number',
|
|
465
|
+
description: 'Current frame number (for tick action)',
|
|
466
|
+
required: false,
|
|
467
|
+
},
|
|
468
|
+
},
|
|
469
|
+
tier: 'free',
|
|
470
|
+
execute: async (args) => {
|
|
471
|
+
const coord = getEngine();
|
|
472
|
+
const action = args.action;
|
|
473
|
+
switch (action) {
|
|
474
|
+
case 'queue_speech': {
|
|
475
|
+
const text = args.text;
|
|
476
|
+
if (!text)
|
|
477
|
+
return 'Error: "text" is required for queue_speech';
|
|
478
|
+
const mood = args.mood || 'calm';
|
|
479
|
+
const priority = args.priority ?? 50;
|
|
480
|
+
const duration = args.duration ?? DEFAULT_SPEECH_DURATION;
|
|
481
|
+
const source = args.source || 'unknown';
|
|
482
|
+
queueSpeech(coord, text, mood, priority, duration, source);
|
|
483
|
+
return `Queued speech: "${text.slice(0, 60)}${text.length > 60 ? '...' : ''}" [P${priority}] from ${source} (${duration} frames). Queue depth: ${coord.speechQueue.length}/${MAX_SPEECH_QUEUE}`;
|
|
484
|
+
}
|
|
485
|
+
case 'request_mood': {
|
|
486
|
+
const mood = args.mood;
|
|
487
|
+
if (!mood)
|
|
488
|
+
return 'Error: "mood" is required for request_mood';
|
|
489
|
+
const priority = args.priority ?? 50;
|
|
490
|
+
const source = args.source || 'unknown';
|
|
491
|
+
const duration = args.duration ?? 360;
|
|
492
|
+
requestMood(coord, mood, priority, source, duration);
|
|
493
|
+
const resolved = resolveMood(coord, coord.frameCount);
|
|
494
|
+
return `Mood requested: ${mood} [P${priority}] from ${source} (${duration} frames). Current winning mood: ${resolved}`;
|
|
495
|
+
}
|
|
496
|
+
case 'post_blackboard': {
|
|
497
|
+
const key = args.key;
|
|
498
|
+
if (!key)
|
|
499
|
+
return 'Error: "key" is required for post_blackboard';
|
|
500
|
+
const source = args.source || 'unknown';
|
|
501
|
+
const ttl = args.ttl ?? 360;
|
|
502
|
+
let value;
|
|
503
|
+
try {
|
|
504
|
+
value = args.value ? JSON.parse(args.value) : null;
|
|
505
|
+
}
|
|
506
|
+
catch {
|
|
507
|
+
value = args.value;
|
|
508
|
+
}
|
|
509
|
+
postToBlackboard(coord, key, value, source, ttl);
|
|
510
|
+
return `Posted to blackboard: ${key} = ${JSON.stringify(value).slice(0, 80)} (from ${source}, TTL ${ttl} frames)`;
|
|
511
|
+
}
|
|
512
|
+
case 'report_status': {
|
|
513
|
+
const engineName = args.engine_name || args.source;
|
|
514
|
+
if (!engineName)
|
|
515
|
+
return 'Error: "engine_name" or "source" is required for report_status';
|
|
516
|
+
const active = args.active ?? true;
|
|
517
|
+
const errors = args.errors ?? 0;
|
|
518
|
+
reportEngineStatus(coord, engineName, active, errors);
|
|
519
|
+
const status = coord.engineStatus.get(engineName);
|
|
520
|
+
return `Engine ${engineName}: ${active ? 'ACTIVE' : 'IDLE'}, ${errors} errors, ${status.outputCount} outputs, ${status.ticksPerSecond.toFixed(1)} tps`;
|
|
521
|
+
}
|
|
522
|
+
case 'tick': {
|
|
523
|
+
const frame = args.frame ?? coord.frameCount + 1;
|
|
524
|
+
const output = tickCoordination(coord, frame);
|
|
525
|
+
const lines = [
|
|
526
|
+
`Frame ${frame} — Coordination Tick`,
|
|
527
|
+
` Speech: ${output.speech ? `"${output.speech.slice(0, 60)}${output.speech.length > 60 ? '...' : ''}"` : '(none)'}`,
|
|
528
|
+
` Mood: ${output.mood}`,
|
|
529
|
+
` Walk: ${output.shouldWalk ? `target=${output.walkTarget}` : 'no'}`,
|
|
530
|
+
` Effects: ${output.effects.length > 0 ? output.effects.join(', ') : '(none)'}`,
|
|
531
|
+
` Announcements: ${output.announcements.length > 0 ? output.announcements.join('; ') : '(none)'}`,
|
|
532
|
+
` Active engines: ${coord.resourceBudget.activeEngines}`,
|
|
533
|
+
` Queue depth: ${coord.speechQueue.length}`,
|
|
534
|
+
];
|
|
535
|
+
return lines.join('\n');
|
|
536
|
+
}
|
|
537
|
+
default:
|
|
538
|
+
return `Unknown action: ${action}. Use: queue_speech, request_mood, post_blackboard, report_status, tick`;
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
//# sourceMappingURL=coordination-engine.js.map
|