@directive-run/knowledge 0.2.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/LICENSE +21 -0
- package/README.md +63 -0
- package/ai/ai-adapters.md +250 -0
- package/ai/ai-agents-streaming.md +269 -0
- package/ai/ai-budget-resilience.md +235 -0
- package/ai/ai-communication.md +281 -0
- package/ai/ai-debug-observability.md +243 -0
- package/ai/ai-guardrails-memory.md +332 -0
- package/ai/ai-mcp-rag.md +288 -0
- package/ai/ai-multi-agent.md +274 -0
- package/ai/ai-orchestrator.md +227 -0
- package/ai/ai-security.md +293 -0
- package/ai/ai-tasks.md +261 -0
- package/ai/ai-testing-evals.md +378 -0
- package/api-skeleton.md +5 -0
- package/core/anti-patterns.md +382 -0
- package/core/constraints.md +263 -0
- package/core/core-patterns.md +228 -0
- package/core/error-boundaries.md +322 -0
- package/core/multi-module.md +315 -0
- package/core/naming.md +283 -0
- package/core/plugins.md +344 -0
- package/core/react-adapter.md +262 -0
- package/core/resolvers.md +357 -0
- package/core/schema-types.md +262 -0
- package/core/system-api.md +271 -0
- package/core/testing.md +257 -0
- package/core/time-travel.md +238 -0
- package/dist/index.cjs +111 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +102 -0
- package/dist/index.js.map +1 -0
- package/examples/ab-testing.ts +385 -0
- package/examples/ai-checkpoint.ts +509 -0
- package/examples/ai-guardrails.ts +319 -0
- package/examples/ai-orchestrator.ts +589 -0
- package/examples/async-chains.ts +287 -0
- package/examples/auth-flow.ts +371 -0
- package/examples/batch-resolver.ts +341 -0
- package/examples/checkers.ts +589 -0
- package/examples/contact-form.ts +176 -0
- package/examples/counter.ts +393 -0
- package/examples/dashboard-loader.ts +512 -0
- package/examples/debounce-constraints.ts +105 -0
- package/examples/dynamic-modules.ts +293 -0
- package/examples/error-boundaries.ts +430 -0
- package/examples/feature-flags.ts +220 -0
- package/examples/form-wizard.ts +347 -0
- package/examples/fraud-analysis.ts +663 -0
- package/examples/goal-heist.ts +341 -0
- package/examples/multi-module.ts +57 -0
- package/examples/newsletter.ts +241 -0
- package/examples/notifications.ts +210 -0
- package/examples/optimistic-updates.ts +317 -0
- package/examples/pagination.ts +260 -0
- package/examples/permissions.ts +337 -0
- package/examples/provider-routing.ts +403 -0
- package/examples/server.ts +316 -0
- package/examples/shopping-cart.ts +422 -0
- package/examples/sudoku.ts +630 -0
- package/examples/theme-locale.ts +204 -0
- package/examples/time-machine.ts +225 -0
- package/examples/topic-guard.ts +306 -0
- package/examples/url-sync.ts +333 -0
- package/examples/websocket.ts +404 -0
- package/package.json +65 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
// Example: ai-orchestrator
|
|
2
|
+
// Source: examples/checkers/src/ai-orchestrator.ts
|
|
3
|
+
// Extracted for AI rules — DOM wiring stripped
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checkers AI Orchestrator
|
|
7
|
+
*
|
|
8
|
+
* Composes directive AI adapter features via explicit wiring:
|
|
9
|
+
*
|
|
10
|
+
* 1. Agent Orchestrator — Manages Claude API via generic AgentRunner + guardrails
|
|
11
|
+
* 2. Memory — Sliding window (30 messages) with auto-summarization
|
|
12
|
+
* 3. Output Guardrail — Validates move JSON schema before accepting
|
|
13
|
+
* 4. Rate Limiter — 10 requests/min to prevent runaway API calls
|
|
14
|
+
* 5. Circuit Breaker — After 3 failures, falls back to local minimax for 30s
|
|
15
|
+
* 6. Cost Tracking — Token count + estimated cost at Haiku rates
|
|
16
|
+
* 7. Streaming — Token-by-token chat delivery with length guardrail
|
|
17
|
+
* 8. Multi-Agent — Parallel move + analysis agents
|
|
18
|
+
* 9. Communication Bus — Agent-to-agent INFORM messages for move/chat events
|
|
19
|
+
* 10. Semantic Cache — Hash-based position caching (0.98 threshold)
|
|
20
|
+
* 11. Observability — Metrics, tracing, alerting dashboard
|
|
21
|
+
* 12. OTLP Exporter — Periodic export to OpenTelemetry collector
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
type AgentLike,
|
|
26
|
+
CircuitBreakerOpenError,
|
|
27
|
+
type InputGuardrailData,
|
|
28
|
+
type NamedGuardrail,
|
|
29
|
+
type RunResult,
|
|
30
|
+
createAgentMemory,
|
|
31
|
+
createAgentOrchestrator,
|
|
32
|
+
createLengthStreamingGuardrail,
|
|
33
|
+
createMessageBus,
|
|
34
|
+
createMultiAgentOrchestrator,
|
|
35
|
+
createOutputSchemaGuardrail,
|
|
36
|
+
createSemanticCache,
|
|
37
|
+
createSlidingWindowStrategy,
|
|
38
|
+
createStreamingRunner,
|
|
39
|
+
createTestEmbedder,
|
|
40
|
+
estimateCost,
|
|
41
|
+
parallel,
|
|
42
|
+
} from "@directive-run/ai";
|
|
43
|
+
import type { CacheStats } from "@directive-run/ai";
|
|
44
|
+
import {
|
|
45
|
+
type CircuitState,
|
|
46
|
+
createAgentMetrics,
|
|
47
|
+
createCircuitBreaker,
|
|
48
|
+
createOTLPExporter,
|
|
49
|
+
createObservability,
|
|
50
|
+
} from "@directive-run/core/plugins";
|
|
51
|
+
import {
|
|
52
|
+
analysisAgent,
|
|
53
|
+
chatAgent,
|
|
54
|
+
formatLegalMoves,
|
|
55
|
+
moveAgent,
|
|
56
|
+
renderBoardForClaude,
|
|
57
|
+
runClaude,
|
|
58
|
+
runClaudeWithCallbacks,
|
|
59
|
+
} from "./claude-adapter.js";
|
|
60
|
+
import type { Board, Move, Player } from "./rules.js";
|
|
61
|
+
import { pickAiMove } from "./rules.js";
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Types
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
export interface MoveResult {
|
|
68
|
+
from: number;
|
|
69
|
+
to: number;
|
|
70
|
+
reasoning: string;
|
|
71
|
+
chat: string;
|
|
72
|
+
analysis: string | null;
|
|
73
|
+
isLocalFallback: boolean;
|
|
74
|
+
isCached: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface MoveWithAnalysis {
|
|
78
|
+
move: { from: number; to: number; reasoning: string; chat: string };
|
|
79
|
+
analysis: string | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface CheckersAI {
|
|
83
|
+
requestMove(
|
|
84
|
+
board: Board,
|
|
85
|
+
player: Player,
|
|
86
|
+
legalMoves: Move[],
|
|
87
|
+
humanMoveDesc?: string,
|
|
88
|
+
): Promise<MoveResult>;
|
|
89
|
+
sendChat(
|
|
90
|
+
message: string,
|
|
91
|
+
onToken?: (token: string) => void,
|
|
92
|
+
): Promise<string | null>;
|
|
93
|
+
reset(): void;
|
|
94
|
+
getState(): {
|
|
95
|
+
isThinking: boolean;
|
|
96
|
+
totalTokens: number;
|
|
97
|
+
estimatedCost: number;
|
|
98
|
+
circuitState: CircuitState;
|
|
99
|
+
memoryMessageCount: number;
|
|
100
|
+
cacheStats: CacheStats;
|
|
101
|
+
busMessageCount: number;
|
|
102
|
+
};
|
|
103
|
+
dispose(): void;
|
|
104
|
+
/** Escape hatch for dashboard rendering */
|
|
105
|
+
readonly observability: ReturnType<typeof createObservability> | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Move Schema Validation
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
function validateMoveOutput(value: unknown): {
|
|
113
|
+
valid: boolean;
|
|
114
|
+
errors?: string[];
|
|
115
|
+
} {
|
|
116
|
+
if (typeof value !== "object" || value === null) {
|
|
117
|
+
return { valid: false, errors: ["Expected an object"] };
|
|
118
|
+
}
|
|
119
|
+
const obj = value as Record<string, unknown>;
|
|
120
|
+
const errors: string[] = [];
|
|
121
|
+
if (typeof obj.from !== "number") errors.push("'from' must be a number");
|
|
122
|
+
if (typeof obj.to !== "number") errors.push("'to' must be a number");
|
|
123
|
+
if (typeof obj.reasoning !== "string")
|
|
124
|
+
errors.push("'reasoning' must be a string");
|
|
125
|
+
if (typeof obj.chat !== "string") errors.push("'chat' must be a string");
|
|
126
|
+
|
|
127
|
+
return errors.length > 0 ? { valid: false, errors } : { valid: true };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Merge function for parallel move + analysis
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
function mergeResults(results: RunResult<unknown>[]): MoveWithAnalysis {
|
|
135
|
+
const moveResult = results[0]?.output as
|
|
136
|
+
| { from: number; to: number; reasoning: string; chat: string }
|
|
137
|
+
| undefined;
|
|
138
|
+
const analysisResult = results[1]?.output as string | undefined;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
move: moveResult ?? {
|
|
142
|
+
from: -1,
|
|
143
|
+
to: -1,
|
|
144
|
+
reasoning: "No result",
|
|
145
|
+
chat: "Something went wrong",
|
|
146
|
+
},
|
|
147
|
+
analysis: analysisResult ?? null,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// Factory
|
|
153
|
+
// ============================================================================
|
|
154
|
+
|
|
155
|
+
export function createCheckersAI(): CheckersAI {
|
|
156
|
+
let isThinking = false;
|
|
157
|
+
let totalTokens = 0;
|
|
158
|
+
const costRatePerMillion = 2.4;
|
|
159
|
+
|
|
160
|
+
// --- Features ---
|
|
161
|
+
|
|
162
|
+
const memory = createAgentMemory({
|
|
163
|
+
strategy: createSlidingWindowStrategy(),
|
|
164
|
+
strategyConfig: { maxMessages: 30, preserveRecentCount: 6 },
|
|
165
|
+
autoManage: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const circuitBreaker = createCircuitBreaker({
|
|
169
|
+
failureThreshold: 3,
|
|
170
|
+
recoveryTimeMs: 30000,
|
|
171
|
+
name: "checkers-ai",
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const cache = createSemanticCache({
|
|
175
|
+
embedder: createTestEmbedder(),
|
|
176
|
+
similarityThreshold: 0.98,
|
|
177
|
+
maxCacheSize: 200,
|
|
178
|
+
ttlMs: 600_000,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const obs = createObservability({
|
|
182
|
+
serviceName: "checkers-ai",
|
|
183
|
+
metrics: { enabled: true },
|
|
184
|
+
tracing: { enabled: true, sampleRate: 1.0 },
|
|
185
|
+
alerts: [
|
|
186
|
+
{ metric: "agent.errors", threshold: 5, operator: ">", action: "warn" },
|
|
187
|
+
{
|
|
188
|
+
metric: "agent.latency",
|
|
189
|
+
threshold: 10000,
|
|
190
|
+
operator: ">",
|
|
191
|
+
action: "warn",
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const metrics = createAgentMetrics(obs);
|
|
197
|
+
|
|
198
|
+
const otlpExporter = createOTLPExporter({
|
|
199
|
+
endpoint: "http://localhost:4318",
|
|
200
|
+
serviceName: "checkers-ai",
|
|
201
|
+
onError: (err) => {
|
|
202
|
+
console.debug(
|
|
203
|
+
`[OTLP] export failed (collector not running?):`,
|
|
204
|
+
err.message,
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const otlpInterval = setInterval(() => {
|
|
210
|
+
try {
|
|
211
|
+
const data = obs.export();
|
|
212
|
+
if (data.metrics.length > 0) otlpExporter.exportMetrics(data.metrics);
|
|
213
|
+
if (data.traces.length > 0) otlpExporter.exportTraces(data.traces);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.debug("[OTLP] periodic export error:", err);
|
|
216
|
+
}
|
|
217
|
+
}, 15_000);
|
|
218
|
+
|
|
219
|
+
const bus = createMessageBus({ maxHistory: 100 });
|
|
220
|
+
|
|
221
|
+
// --- Rate limiter as input guardrail ---
|
|
222
|
+
const rateLimitTimestamps: number[] = [];
|
|
223
|
+
let rateLimitStartIdx = 0;
|
|
224
|
+
const MAX_PER_MINUTE = 10;
|
|
225
|
+
|
|
226
|
+
const rateLimitGuardrail: NamedGuardrail<InputGuardrailData> = {
|
|
227
|
+
name: "rate-limit",
|
|
228
|
+
fn: () => {
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
const windowStart = now - 60_000;
|
|
231
|
+
while (
|
|
232
|
+
rateLimitStartIdx < rateLimitTimestamps.length &&
|
|
233
|
+
rateLimitTimestamps[rateLimitStartIdx]! < windowStart
|
|
234
|
+
) {
|
|
235
|
+
rateLimitStartIdx++;
|
|
236
|
+
}
|
|
237
|
+
if (
|
|
238
|
+
rateLimitStartIdx > rateLimitTimestamps.length / 2 &&
|
|
239
|
+
rateLimitStartIdx > 100
|
|
240
|
+
) {
|
|
241
|
+
rateLimitTimestamps.splice(0, rateLimitStartIdx);
|
|
242
|
+
rateLimitStartIdx = 0;
|
|
243
|
+
}
|
|
244
|
+
const active = rateLimitTimestamps.length - rateLimitStartIdx;
|
|
245
|
+
if (active >= MAX_PER_MINUTE) {
|
|
246
|
+
return {
|
|
247
|
+
passed: false,
|
|
248
|
+
reason: `Rate limit exceeded (${MAX_PER_MINUTE}/min)`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
rateLimitTimestamps.push(now);
|
|
252
|
+
|
|
253
|
+
return { passed: true };
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const moveSchemaGuardrail = createOutputSchemaGuardrail({
|
|
258
|
+
validate: validateMoveOutput,
|
|
259
|
+
errorPrefix: "Invalid move response",
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// --- Core orchestrator ---
|
|
263
|
+
const orchestrator = createAgentOrchestrator({
|
|
264
|
+
runner: runClaude,
|
|
265
|
+
maxTokenBudget: 50000,
|
|
266
|
+
memory,
|
|
267
|
+
circuitBreaker,
|
|
268
|
+
guardrails: {
|
|
269
|
+
input: [rateLimitGuardrail],
|
|
270
|
+
output: [moveSchemaGuardrail],
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// --- Multi-agent ---
|
|
275
|
+
const agentRegistry = {
|
|
276
|
+
move: {
|
|
277
|
+
agent: moveAgent,
|
|
278
|
+
description: "Selects the best move",
|
|
279
|
+
capabilities: ["move"] as string[],
|
|
280
|
+
},
|
|
281
|
+
chat: {
|
|
282
|
+
agent: chatAgent,
|
|
283
|
+
description: "Free-form chat",
|
|
284
|
+
capabilities: ["chat"] as string[],
|
|
285
|
+
},
|
|
286
|
+
analysis: {
|
|
287
|
+
agent: analysisAgent,
|
|
288
|
+
description: "Strategic analysis",
|
|
289
|
+
capabilities: ["analysis"] as string[],
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const multi = createMultiAgentOrchestrator({
|
|
294
|
+
runner: runClaude,
|
|
295
|
+
agents: agentRegistry,
|
|
296
|
+
patterns: {
|
|
297
|
+
moveWithAnalysis: parallel<MoveWithAnalysis>(
|
|
298
|
+
["move", "analysis"],
|
|
299
|
+
mergeResults,
|
|
300
|
+
{ minSuccess: 1, timeout: 15000 },
|
|
301
|
+
),
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// --- Streaming runner ---
|
|
306
|
+
const streamingRunner = createStreamingRunner(runClaudeWithCallbacks, {
|
|
307
|
+
streamingGuardrails: [createLengthStreamingGuardrail({ maxTokens: 500 })],
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// --- Helpers ---
|
|
311
|
+
|
|
312
|
+
function resolveAgent(agentId: string): AgentLike {
|
|
313
|
+
const reg = agentRegistry[agentId as keyof typeof agentRegistry];
|
|
314
|
+
if (!reg) {
|
|
315
|
+
throw new Error(`[CheckersAI] Agent "${agentId}" not found`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return reg.agent;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildMoveInput(
|
|
322
|
+
board: Board,
|
|
323
|
+
player: Player,
|
|
324
|
+
legalMoves: Move[],
|
|
325
|
+
humanMoveDesc?: string,
|
|
326
|
+
): string {
|
|
327
|
+
const boardStr = renderBoardForClaude(board);
|
|
328
|
+
const movesStr = formatLegalMoves(legalMoves);
|
|
329
|
+
let input = "";
|
|
330
|
+
if (humanMoveDesc) {
|
|
331
|
+
input += `Human's move: ${humanMoveDesc}\n\n`;
|
|
332
|
+
}
|
|
333
|
+
input += `Current board:\n${boardStr}\n\nYour legal moves (you MUST pick one):\n${movesStr}\n\nPick your move.`;
|
|
334
|
+
|
|
335
|
+
return input;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function localFallback(
|
|
339
|
+
board: Board,
|
|
340
|
+
player: Player,
|
|
341
|
+
legalMoves: Move[],
|
|
342
|
+
reason: string,
|
|
343
|
+
): MoveResult {
|
|
344
|
+
const move = pickAiMove(board, player) ?? legalMoves[0];
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
from: move.from,
|
|
348
|
+
to: move.to,
|
|
349
|
+
reasoning: `Local AI: ${reason}`,
|
|
350
|
+
chat: reason,
|
|
351
|
+
analysis: null,
|
|
352
|
+
isLocalFallback: true,
|
|
353
|
+
isCached: false,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// --- Public API ---
|
|
358
|
+
|
|
359
|
+
async function requestMove(
|
|
360
|
+
board: Board,
|
|
361
|
+
player: Player,
|
|
362
|
+
legalMoves: Move[],
|
|
363
|
+
humanMoveDesc?: string,
|
|
364
|
+
): Promise<MoveResult> {
|
|
365
|
+
if (legalMoves.length === 0) {
|
|
366
|
+
return {
|
|
367
|
+
from: -1,
|
|
368
|
+
to: -1,
|
|
369
|
+
reasoning: "No moves",
|
|
370
|
+
chat: "No moves!",
|
|
371
|
+
analysis: null,
|
|
372
|
+
isLocalFallback: true,
|
|
373
|
+
isCached: false,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
isThinking = true;
|
|
378
|
+
const input = buildMoveInput(board, player, legalMoves, humanMoveDesc);
|
|
379
|
+
|
|
380
|
+
// Cache check
|
|
381
|
+
try {
|
|
382
|
+
const cached = await cache.lookup(input, "moveWithAnalysis");
|
|
383
|
+
if (cached.hit && cached.entry) {
|
|
384
|
+
obs.incrementCounter("cache.hits");
|
|
385
|
+
try {
|
|
386
|
+
const parsed = JSON.parse(cached.entry.response) as MoveWithAnalysis;
|
|
387
|
+
isThinking = false;
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
from: parsed.move.from,
|
|
391
|
+
to: parsed.move.to,
|
|
392
|
+
reasoning: parsed.move.reasoning,
|
|
393
|
+
chat: parsed.move.chat,
|
|
394
|
+
analysis: parsed.analysis,
|
|
395
|
+
isLocalFallback: false,
|
|
396
|
+
isCached: true,
|
|
397
|
+
};
|
|
398
|
+
} catch {
|
|
399
|
+
// Invalid cache entry — fall through
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
obs.incrementCounter("cache.misses");
|
|
403
|
+
} catch {
|
|
404
|
+
// Cache lookup failed — treat as miss
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const span = obs.startSpan("pattern.moveWithAnalysis");
|
|
408
|
+
const startTime = Date.now();
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const result = await multi.runPattern<MoveWithAnalysis>(
|
|
412
|
+
"moveWithAnalysis",
|
|
413
|
+
input,
|
|
414
|
+
);
|
|
415
|
+
const latencyMs = Date.now() - startTime;
|
|
416
|
+
const parsed = result.move;
|
|
417
|
+
|
|
418
|
+
// Track metrics
|
|
419
|
+
obs.endSpan(span.spanId, "ok");
|
|
420
|
+
metrics.trackRun("moveWithAnalysis", { success: true, latencyMs });
|
|
421
|
+
|
|
422
|
+
// Cache store
|
|
423
|
+
try {
|
|
424
|
+
await cache.store(input, JSON.stringify(result), "moveWithAnalysis");
|
|
425
|
+
} catch {
|
|
426
|
+
// Non-fatal
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Bus publish
|
|
430
|
+
bus.publish({
|
|
431
|
+
type: "INFORM",
|
|
432
|
+
from: "moveWithAnalysis",
|
|
433
|
+
to: "*",
|
|
434
|
+
topic: "moveWithAnalysis.completed",
|
|
435
|
+
content: {},
|
|
436
|
+
} as Parameters<typeof bus.publish>[0]);
|
|
437
|
+
|
|
438
|
+
// Validate the move is legal
|
|
439
|
+
const isLegal = legalMoves.some(
|
|
440
|
+
(m) => m.from === parsed.from && m.to === parsed.to,
|
|
441
|
+
);
|
|
442
|
+
if (!isLegal) {
|
|
443
|
+
console.warn(
|
|
444
|
+
"[CheckersAI] Illegal move returned, falling back",
|
|
445
|
+
parsed,
|
|
446
|
+
);
|
|
447
|
+
isThinking = false;
|
|
448
|
+
|
|
449
|
+
return localFallback(
|
|
450
|
+
board,
|
|
451
|
+
player,
|
|
452
|
+
legalMoves,
|
|
453
|
+
"Hmm, I tried an illegal move. Let me pick again!",
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
isThinking = false;
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
from: parsed.from,
|
|
461
|
+
to: parsed.to,
|
|
462
|
+
reasoning: parsed.reasoning,
|
|
463
|
+
chat: parsed.chat,
|
|
464
|
+
analysis: result.analysis,
|
|
465
|
+
isLocalFallback: false,
|
|
466
|
+
isCached: false,
|
|
467
|
+
};
|
|
468
|
+
} catch (err) {
|
|
469
|
+
isThinking = false;
|
|
470
|
+
const latencyMs = Date.now() - startTime;
|
|
471
|
+
obs.endSpan(span.spanId, "error");
|
|
472
|
+
metrics.trackRun("moveWithAnalysis", { success: false, latencyMs });
|
|
473
|
+
|
|
474
|
+
if (err instanceof CircuitBreakerOpenError) {
|
|
475
|
+
return localFallback(
|
|
476
|
+
board,
|
|
477
|
+
player,
|
|
478
|
+
legalMoves,
|
|
479
|
+
"Circuit breaker open — using local AI while I recover.",
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
console.error("[CheckersAI] Move error:", err);
|
|
484
|
+
const msg = err instanceof Error ? err.message : "Unknown error";
|
|
485
|
+
|
|
486
|
+
return localFallback(
|
|
487
|
+
board,
|
|
488
|
+
player,
|
|
489
|
+
legalMoves,
|
|
490
|
+
`Error: ${msg}. Using local AI.`,
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function sendChat(
|
|
496
|
+
message: string,
|
|
497
|
+
onToken?: (token: string) => void,
|
|
498
|
+
): Promise<string | null> {
|
|
499
|
+
try {
|
|
500
|
+
const agent = resolveAgent("chat");
|
|
501
|
+
let reply: string;
|
|
502
|
+
|
|
503
|
+
if (onToken && streamingRunner) {
|
|
504
|
+
// Streaming: token-by-token delivery
|
|
505
|
+
const { stream, result } = streamingRunner<string>(agent, message);
|
|
506
|
+
for await (const chunk of stream) {
|
|
507
|
+
if (chunk.type === "token" && chunk.data) onToken(chunk.data);
|
|
508
|
+
}
|
|
509
|
+
const finalResult = await result;
|
|
510
|
+
totalTokens += finalResult.totalTokens;
|
|
511
|
+
reply =
|
|
512
|
+
typeof finalResult.output === "string"
|
|
513
|
+
? finalResult.output
|
|
514
|
+
: String(finalResult.output);
|
|
515
|
+
} else {
|
|
516
|
+
// Non-streaming: skip output guardrails for chat
|
|
517
|
+
const result = await orchestrator.run<string>(agent, message, {
|
|
518
|
+
outputGuardrails: [],
|
|
519
|
+
});
|
|
520
|
+
totalTokens += result.totalTokens;
|
|
521
|
+
reply = result.output;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return reply;
|
|
525
|
+
} catch (err) {
|
|
526
|
+
if (err instanceof CircuitBreakerOpenError) {
|
|
527
|
+
return "I'm having trouble connecting right now. Try again in a bit!";
|
|
528
|
+
}
|
|
529
|
+
console.error("[CheckersAI] Chat error:", err);
|
|
530
|
+
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function reset(): void {
|
|
536
|
+
memory.clear();
|
|
537
|
+
circuitBreaker.reset();
|
|
538
|
+
cache.clear();
|
|
539
|
+
obs.clear();
|
|
540
|
+
bus.clear();
|
|
541
|
+
multi.reset();
|
|
542
|
+
orchestrator.reset();
|
|
543
|
+
rateLimitTimestamps.length = 0;
|
|
544
|
+
rateLimitStartIdx = 0;
|
|
545
|
+
totalTokens = 0;
|
|
546
|
+
isThinking = false;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function getState() {
|
|
550
|
+
return {
|
|
551
|
+
isThinking,
|
|
552
|
+
totalTokens,
|
|
553
|
+
estimatedCost:
|
|
554
|
+
costRatePerMillion > 0
|
|
555
|
+
? estimateCost(totalTokens, costRatePerMillion)
|
|
556
|
+
: 0,
|
|
557
|
+
circuitState: circuitBreaker.getState(),
|
|
558
|
+
memoryMessageCount: memory.getState()?.messages?.length ?? 0,
|
|
559
|
+
cacheStats: cache.getStats(),
|
|
560
|
+
busMessageCount: bus.getHistory()?.length ?? 0,
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function dispose(): void {
|
|
565
|
+
clearInterval(otlpInterval);
|
|
566
|
+
// Flush OTLP one final time
|
|
567
|
+
try {
|
|
568
|
+
const data = obs.export();
|
|
569
|
+
if (data.metrics.length > 0) otlpExporter.exportMetrics(data.metrics);
|
|
570
|
+
if (data.traces.length > 0) otlpExporter.exportTraces(data.traces);
|
|
571
|
+
} catch {
|
|
572
|
+
// Best-effort flush on dispose
|
|
573
|
+
}
|
|
574
|
+
orchestrator.dispose();
|
|
575
|
+
multi.dispose();
|
|
576
|
+
obs.dispose();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
requestMove,
|
|
581
|
+
sendChat,
|
|
582
|
+
reset,
|
|
583
|
+
getState,
|
|
584
|
+
dispose,
|
|
585
|
+
get observability() {
|
|
586
|
+
return obs;
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
}
|