@gonzih/cc-discord 0.2.24 → 0.2.25
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/loop-manager.d.ts +66 -0
- package/dist/loop-manager.js +118 -0
- package/package.json +1 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop observability for cc-discord meta-agent channels.
|
|
3
|
+
*
|
|
4
|
+
* When a user sends a goal-oriented message to a meta-agent channel the bot
|
|
5
|
+
* creates a Discord thread on that message, tracks iteration state in Redis,
|
|
6
|
+
* and lets human operators steer the loop via 🔄/✅/❌ reactions.
|
|
7
|
+
*
|
|
8
|
+
* Redis layout:
|
|
9
|
+
* cca:discord:loop:{channelId}:{threadId} HASH TTL 24h
|
|
10
|
+
* goal → string
|
|
11
|
+
* namespace → string
|
|
12
|
+
* iteration → stringified number
|
|
13
|
+
* max_iterations → stringified number
|
|
14
|
+
* thread_id → string
|
|
15
|
+
* gate_failures → JSON array of GateFailure
|
|
16
|
+
*/
|
|
17
|
+
import type { Redis } from "ioredis";
|
|
18
|
+
export interface GateFailure {
|
|
19
|
+
gate: string;
|
|
20
|
+
feedback: string;
|
|
21
|
+
iteration: number;
|
|
22
|
+
confidence: number;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
}
|
|
25
|
+
export interface LoopState {
|
|
26
|
+
channelId: string;
|
|
27
|
+
threadId: string;
|
|
28
|
+
/** ID of the first message posted inside the thread (reactions live here). */
|
|
29
|
+
goalMessageId: string;
|
|
30
|
+
namespace: string;
|
|
31
|
+
goal: string;
|
|
32
|
+
iteration: number;
|
|
33
|
+
maxIterations: number;
|
|
34
|
+
gateFailures: GateFailure[];
|
|
35
|
+
}
|
|
36
|
+
export interface EvalReport {
|
|
37
|
+
gate: string;
|
|
38
|
+
passed: boolean;
|
|
39
|
+
feedback: string;
|
|
40
|
+
iteration: number;
|
|
41
|
+
maxIterations: number;
|
|
42
|
+
confidence: number;
|
|
43
|
+
}
|
|
44
|
+
/** Return true when `text` looks like an action goal rather than a question or chat. */
|
|
45
|
+
export declare function isGoalMessage(text: string): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Parse an `eval_report` object embedded in a raw notification JSON string.
|
|
48
|
+
* Returns null when the field is absent, malformed, or the input is not JSON.
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseEvalReport(raw: string): EvalReport | null;
|
|
51
|
+
export declare class LoopManager {
|
|
52
|
+
private redis;
|
|
53
|
+
/** channelId → LoopState */
|
|
54
|
+
private activeLoops;
|
|
55
|
+
/** reactionMessageId → channelId — for O(1) lookup on reaction events */
|
|
56
|
+
private reactionMessageMap;
|
|
57
|
+
constructor(redis: Redis);
|
|
58
|
+
startLoop(channelId: string, threadId: string, goalMessageId: string, namespace: string, goal: string, maxIterations?: number): Promise<LoopState>;
|
|
59
|
+
isActive(channelId: string): boolean;
|
|
60
|
+
getState(channelId: string): LoopState | undefined;
|
|
61
|
+
getThreadId(channelId: string): string | undefined;
|
|
62
|
+
/** Look up the main channel ID from a reaction message ID. */
|
|
63
|
+
getChannelIdByReactionMessage(messageId: string): string | undefined;
|
|
64
|
+
addGateFailure(channelId: string, failure: GateFailure): Promise<void>;
|
|
65
|
+
endLoop(channelId: string): Promise<void>;
|
|
66
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loop observability for cc-discord meta-agent channels.
|
|
3
|
+
*
|
|
4
|
+
* When a user sends a goal-oriented message to a meta-agent channel the bot
|
|
5
|
+
* creates a Discord thread on that message, tracks iteration state in Redis,
|
|
6
|
+
* and lets human operators steer the loop via 🔄/✅/❌ reactions.
|
|
7
|
+
*
|
|
8
|
+
* Redis layout:
|
|
9
|
+
* cca:discord:loop:{channelId}:{threadId} HASH TTL 24h
|
|
10
|
+
* goal → string
|
|
11
|
+
* namespace → string
|
|
12
|
+
* iteration → stringified number
|
|
13
|
+
* max_iterations → stringified number
|
|
14
|
+
* thread_id → string
|
|
15
|
+
* gate_failures → JSON array of GateFailure
|
|
16
|
+
*/
|
|
17
|
+
const LOOP_TTL = 86_400; // 24h in seconds
|
|
18
|
+
function loopKey(channelId, threadId) {
|
|
19
|
+
return `cca:discord:loop:${channelId}:${threadId}`;
|
|
20
|
+
}
|
|
21
|
+
// Action-verb heuristic: messages that start with one of these words are treated as goals.
|
|
22
|
+
const GOAL_RE = /^(create|build|write|implement|fix|add|make|generate|refactor|set\s+up|update|deploy|run|test|check|migrate|convert|analyze|summarize|extract|draft|scaffold|configure|install|delete|remove|optimize|debug|review|audit|plan|design|port|bump|release)\b/i;
|
|
23
|
+
/** Return true when `text` looks like an action goal rather than a question or chat. */
|
|
24
|
+
export function isGoalMessage(text) {
|
|
25
|
+
return GOAL_RE.test(text.trim());
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse an `eval_report` object embedded in a raw notification JSON string.
|
|
29
|
+
* Returns null when the field is absent, malformed, or the input is not JSON.
|
|
30
|
+
*/
|
|
31
|
+
export function parseEvalReport(raw) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(raw);
|
|
34
|
+
const r = parsed.eval_report;
|
|
35
|
+
if (!r || typeof r.gate !== "string" || typeof r.passed !== "boolean")
|
|
36
|
+
return null;
|
|
37
|
+
return {
|
|
38
|
+
gate: r.gate,
|
|
39
|
+
passed: r.passed,
|
|
40
|
+
feedback: typeof r.feedback === "string" ? r.feedback : "",
|
|
41
|
+
iteration: typeof r.iteration === "number" ? r.iteration : 0,
|
|
42
|
+
maxIterations: typeof r.max_iterations === "number" ? r.max_iterations : 0,
|
|
43
|
+
confidence: typeof r.confidence === "number" ? r.confidence : 0,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export class LoopManager {
|
|
51
|
+
redis;
|
|
52
|
+
/** channelId → LoopState */
|
|
53
|
+
activeLoops = new Map();
|
|
54
|
+
/** reactionMessageId → channelId — for O(1) lookup on reaction events */
|
|
55
|
+
reactionMessageMap = new Map();
|
|
56
|
+
constructor(redis) {
|
|
57
|
+
this.redis = redis;
|
|
58
|
+
}
|
|
59
|
+
async startLoop(channelId, threadId, goalMessageId, namespace, goal, maxIterations = 10) {
|
|
60
|
+
const state = {
|
|
61
|
+
channelId,
|
|
62
|
+
threadId,
|
|
63
|
+
goalMessageId,
|
|
64
|
+
namespace,
|
|
65
|
+
goal,
|
|
66
|
+
iteration: 0,
|
|
67
|
+
maxIterations,
|
|
68
|
+
gateFailures: [],
|
|
69
|
+
};
|
|
70
|
+
this.activeLoops.set(channelId, state);
|
|
71
|
+
this.reactionMessageMap.set(goalMessageId, channelId);
|
|
72
|
+
const key = loopKey(channelId, threadId);
|
|
73
|
+
await this.redis.hset(key, {
|
|
74
|
+
goal,
|
|
75
|
+
namespace,
|
|
76
|
+
iteration: "0",
|
|
77
|
+
max_iterations: String(maxIterations),
|
|
78
|
+
thread_id: threadId,
|
|
79
|
+
gate_failures: "[]",
|
|
80
|
+
});
|
|
81
|
+
await this.redis.expire(key, LOOP_TTL);
|
|
82
|
+
return state;
|
|
83
|
+
}
|
|
84
|
+
isActive(channelId) {
|
|
85
|
+
return this.activeLoops.has(channelId);
|
|
86
|
+
}
|
|
87
|
+
getState(channelId) {
|
|
88
|
+
return this.activeLoops.get(channelId);
|
|
89
|
+
}
|
|
90
|
+
getThreadId(channelId) {
|
|
91
|
+
return this.activeLoops.get(channelId)?.threadId;
|
|
92
|
+
}
|
|
93
|
+
/** Look up the main channel ID from a reaction message ID. */
|
|
94
|
+
getChannelIdByReactionMessage(messageId) {
|
|
95
|
+
return this.reactionMessageMap.get(messageId);
|
|
96
|
+
}
|
|
97
|
+
async addGateFailure(channelId, failure) {
|
|
98
|
+
const state = this.activeLoops.get(channelId);
|
|
99
|
+
if (!state)
|
|
100
|
+
return;
|
|
101
|
+
state.gateFailures.push(failure);
|
|
102
|
+
state.iteration++;
|
|
103
|
+
const key = loopKey(channelId, state.threadId);
|
|
104
|
+
await this.redis.hset(key, {
|
|
105
|
+
iteration: String(state.iteration),
|
|
106
|
+
gate_failures: JSON.stringify(state.gateFailures),
|
|
107
|
+
});
|
|
108
|
+
await this.redis.expire(key, LOOP_TTL);
|
|
109
|
+
}
|
|
110
|
+
async endLoop(channelId) {
|
|
111
|
+
const state = this.activeLoops.get(channelId);
|
|
112
|
+
if (state) {
|
|
113
|
+
this.reactionMessageMap.delete(state.goalMessageId);
|
|
114
|
+
await this.redis.del(loopKey(channelId, state.threadId));
|
|
115
|
+
}
|
|
116
|
+
this.activeLoops.delete(channelId);
|
|
117
|
+
}
|
|
118
|
+
}
|