-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathimprovements.ts
More file actions
89 lines (83 loc) · 5.78 KB
/
Copy pathimprovements.ts
File metadata and controls
89 lines (83 loc) · 5.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import { now, q, q1, run, type Row } from "./db.ts";
import { rememberForAgent } from "./memory.ts";
const CHECK_EVERY_MS = Number(process.env.IMPROVEMENT_INTERVAL_MS || 60 * 60_000);
const frustration = /\b(frustrat|annoy|angry|wrong again|not what i asked|stop doing|why (?:do|did) you|i already (?:said|told)|listen to me)\b/i;
const correction = /\b(no,? |actually|instead|do not|don't|never|always|i prefer|please (?:stop|use|remember))\b/i;
export async function runImprovementPass(agentId?: number): Promise<number> {
const agents = agentId
? q("SELECT a.*,ac.channel_id FROM agents a LEFT JOIN agent_channels ac ON ac.agent_id=a.id WHERE a.id=? AND a.status<>'deleted'", agentId)
: q("SELECT a.*,ac.channel_id FROM agents a LEFT JOIN agent_channels ac ON ac.agent_id=a.id WHERE a.kind='channel' AND a.status NOT IN ('deleted','archived')");
let improved = 0;
let reviewed = 0;
let signalsSeen = 0;
const skipper = q1("SELECT a.*, NULL channel_id FROM agents a WHERE a.kind='skipper' AND a.status<>'deleted' LIMIT 1");
const improvedNames: string[] = [];
for (const agent of agents) {
const channelId = Number(agent.channel_id || 0);
if (!channelId) continue;
reviewed++;
const checkpoint = q1("SELECT last_message_id,last_run FROM improvement_checkpoints WHERE agent_id=?", agent.id);
const since = Number(checkpoint?.last_message_id || 0);
const messages = q(`SELECT m.id,m.body,m.created,u.display FROM messages m LEFT JOIN users u ON u.id=m.user_id
WHERE m.channel_id=? AND m.user_id IS NOT NULL AND m.id>? ORDER BY m.id DESC LIMIT 40`, channelId, since).reverse();
const latest = Number(messages.at(-1)?.id || since);
if (messages.length) {
const signals = messages.filter((message) => frustration.test(String(message.body)) || correction.test(String(message.body)));
signalsSeen += signals.length;
if (signals.length) {
const hasFrustration = signals.some((message) => frustration.test(String(message.body)));
const instruction = hasFrustration
? "Treat corrections as high-priority evidence: acknowledge the concrete mismatch, change course immediately, verify the requested outcome, and do not make the user repeat context already present in the thread."
: "Carry explicit user preferences and corrections into future turns; reflect the corrected constraint before acting and verify that the result follows it.";
const duplicate = q1("SELECT id FROM agent_improvements WHERE agent_id=? AND instruction=? AND status='active'", agent.id, instruction);
if (!duplicate) {
const source = signals.at(-1)!;
run(`INSERT INTO agent_improvements (agent_id,channel_id,kind,summary,instruction,source_message_id,status,created)
VALUES (?,?, 'interaction', ?,?,?, 'active',?)`, agent.id, channelId,
`Skipper reviewed recent interaction signals and strengthened @${agent.name}'s response to corrections.`, instruction, source.id, now());
run("INSERT INTO channel_activity (channel_id,kind,summary,status,actor_type,created) VALUES (?,'improvement',?,'complete','skipper',?)",
channelId, `Skipper improved @${agent.name}: future turns will adapt faster to corrections and verify the requested outcome.`, now());
await rememberForAgent(agent, instruction, { source: `skipper-improvement:message:${source.id}`, importance: 0.9, metadata: { kind: "behavior-improvement", channel_id: channelId } });
if (skipper) await rememberForAgent(skipper, `In #${String(q1("SELECT name FROM channels WHERE id=?", channelId)?.name || channelId)}, @${agent.name} was improved: ${instruction}`,
{ source: `workspace-improvement:agent:${agent.id}`, importance: 0.8, metadata: { channel_id: channelId, agent_id: agent.id } });
improved++;
improvedNames.push(`@${agent.name}`);
}
}
}
run(`INSERT INTO improvement_checkpoints (agent_id,last_message_id,last_run) VALUES (?,?,?)
ON CONFLICT(agent_id) DO UPDATE SET last_message_id=MAX(improvement_checkpoints.last_message_id,excluded.last_message_id),last_run=excluded.last_run`, agent.id, latest, now());
}
// Workspace breadcrumb on #main so Activity surfaces quiet/hourly Skipper reviews.
// Skip single-agent scheduleAgentReview noise (thread resolve triggers) when nothing changed.
const main = q1(`SELECT c.id FROM channels c JOIN users u ON u.id=c.personal_main_owner_id
WHERE c.name='main' AND c.kind='channel' AND c.status='active' AND u.is_admin=1 ORDER BY u.id,c.id LIMIT 1`);
if (main && (!agentId || improved > 0)) {
const summary = improved
? `Skipper improvement pass: strengthened ${improved} agent(s) (${improvedNames.slice(0, 6).join(", ")}).`
: `Skipper improvement pass: reviewed ${reviewed} agent(s), ${signalsSeen} correction signal(s); no new durable guidance.`;
run(
"INSERT INTO channel_activity (channel_id,kind,summary,status,actor_type,created) VALUES (?,'improvement',?,?, 'skipper',?)",
main.id,
summary.slice(0, 500),
improved ? "complete" : "quiet",
now(),
);
}
return improved;
}
let timer: NodeJS.Timeout | null = null;
/** A background pass owns its own failures; an unhandled rejection here would
* take down the host over an optional memory write. */
const backgroundPass = (agentId?: number): void => {
void runImprovementPass(agentId).catch((error) => console.warn("Skipper improvement pass failed:", (error as Error).message));
};
export function startImprovementLoop(): void {
if (timer) return;
setTimeout(() => backgroundPass(), Math.min(30_000, CHECK_EVERY_MS)).unref();
timer = setInterval(() => backgroundPass(), Math.max(60_000, CHECK_EVERY_MS));
timer.unref();
}
export function scheduleAgentReview(agentId: number): void {
setTimeout(() => backgroundPass(agentId), 250).unref();
}