clementine-agent 1.0.76 → 1.0.78
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/agent/insight-engine.js +3 -2
- package/dist/agent/proactive-engine.d.ts +50 -0
- package/dist/agent/proactive-engine.js +256 -0
- package/dist/agent/proactive-ledger.d.ts +44 -0
- package/dist/agent/proactive-ledger.js +176 -0
- package/dist/agent/self-improve.js +3 -5
- package/dist/gateway/agent-heartbeat-manager.d.ts +42 -0
- package/dist/gateway/agent-heartbeat-manager.js +123 -0
- package/dist/gateway/agent-heartbeat-scheduler.d.ts +48 -0
- package/dist/gateway/agent-heartbeat-scheduler.js +223 -0
- package/dist/gateway/cron-scheduler.d.ts +1 -0
- package/dist/gateway/cron-scheduler.js +41 -6
- package/dist/gateway/heartbeat-scheduler.d.ts +2 -0
- package/dist/gateway/heartbeat-scheduler.js +239 -76
- package/dist/index.js +34 -20
- package/dist/tools/session-tools.js +63 -6
- package/dist/types.d.ts +14 -0
- package/package.json +1 -1
|
@@ -5,6 +5,15 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { z } from 'zod';
|
|
7
7
|
import { BASE_DIR, HANDOFFS_DIR, INBOX_DIR, TASKS_FILE, logger, textResult, todayStr, listAllGoals, } from './shared.js';
|
|
8
|
+
import { decideDiscoveredWorkItem } from '../agent/proactive-engine.js';
|
|
9
|
+
import { recentDecisions } from '../agent/proactive-ledger.js';
|
|
10
|
+
const ACTION_LABEL = {
|
|
11
|
+
act_now: 'Doing now',
|
|
12
|
+
queue: 'Queued',
|
|
13
|
+
ask_user: 'Needs you',
|
|
14
|
+
snooze: 'Snoozed',
|
|
15
|
+
ignore: 'Skipped',
|
|
16
|
+
};
|
|
8
17
|
function ensureHandoffsDir() {
|
|
9
18
|
if (!existsSync(HANDOFFS_DIR))
|
|
10
19
|
mkdirSync(HANDOFFS_DIR, { recursive: true });
|
|
@@ -78,7 +87,7 @@ export function registerSessionTools(server) {
|
|
|
78
87
|
const staleThreshold = goal.reviewFrequency === 'daily' ? 1 : goal.reviewFrequency === 'weekly' ? 7 : 30;
|
|
79
88
|
if (daysSinceUpdate > staleThreshold) {
|
|
80
89
|
const urgency = Math.min(5, Math.floor(daysSinceUpdate / staleThreshold) + (goal.priority === 'high' ? 2 : goal.priority === 'medium' ? 1 : 0));
|
|
81
|
-
items.push({ type: 'stale-goal', urgency, description: `Goal "${goal.title}" stale for ${daysSinceUpdate}d (${goal.priority} priority)` });
|
|
90
|
+
items.push({ type: 'stale-goal', id: goal.id, urgency, description: `Goal "${goal.title}" stale for ${daysSinceUpdate}d (${goal.priority} priority)` });
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
// 2. Failing cron jobs
|
|
@@ -92,7 +101,7 @@ export function registerSessionTools(server) {
|
|
|
92
101
|
const errCount = consecutiveErrors === -1 ? recent.length : consecutiveErrors;
|
|
93
102
|
if (errCount >= 2) {
|
|
94
103
|
const jobName = recent[0]?.jobName ?? f.replace('.jsonl', '');
|
|
95
|
-
items.push({ type: 'cron-failure', urgency: Math.min(5, errCount), description: `Job "${jobName}" has ${errCount} consecutive failures` });
|
|
104
|
+
items.push({ type: 'cron-failure', id: jobName, urgency: Math.min(5, errCount), description: `Job "${jobName}" has ${errCount} consecutive failures` });
|
|
96
105
|
}
|
|
97
106
|
}
|
|
98
107
|
catch {
|
|
@@ -107,13 +116,13 @@ export function registerSessionTools(server) {
|
|
|
107
116
|
const overdue = (content.match(/- \[ \].*?📅\s*(\d{4}-\d{2}-\d{2})/g) ?? [])
|
|
108
117
|
.filter(line => { const m = line.match(/📅\s*(\d{4}-\d{2}-\d{2})/); return m && m[1] < today; });
|
|
109
118
|
if (overdue.length > 0)
|
|
110
|
-
items.push({ type: 'overdue-tasks', urgency: 4, description: `${overdue.length} overdue task(s) in TASKS.md` });
|
|
119
|
+
items.push({ type: 'overdue-tasks', id: 'tasks-overdue', urgency: 4, description: `${overdue.length} overdue task(s) in TASKS.md` });
|
|
111
120
|
}
|
|
112
121
|
// 4. Inbox items
|
|
113
122
|
if (existsSync(INBOX_DIR)) {
|
|
114
123
|
const inboxFiles = readdirSync(INBOX_DIR).filter(f => f.endsWith('.md') && !f.startsWith('_'));
|
|
115
124
|
if (inboxFiles.length > 0)
|
|
116
|
-
items.push({ type: 'inbox', urgency: 2, description: `${inboxFiles.length} unprocessed inbox item(s)` });
|
|
125
|
+
items.push({ type: 'inbox', id: 'inbox', urgency: 2, description: `${inboxFiles.length} unprocessed inbox item(s)` });
|
|
117
126
|
}
|
|
118
127
|
// 5. Daily plan priorities
|
|
119
128
|
const planFile = path.join(BASE_DIR, 'plans', 'daily', `${todayStr()}.json`);
|
|
@@ -123,7 +132,7 @@ export function registerSessionTools(server) {
|
|
|
123
132
|
for (const p of (plan.priorities ?? []).slice(0, 5)) {
|
|
124
133
|
const alreadyListed = items.some(i => i.description.includes(p.id) || i.description.includes(p.action?.slice(0, 20)));
|
|
125
134
|
if (!alreadyListed)
|
|
126
|
-
items.push({ type: `plan-${p.type}`, urgency: p.urgency ?? 3, description: `[From daily plan] ${p.action}` });
|
|
135
|
+
items.push({ type: `plan-${p.type}`, id: p.id, urgency: p.urgency ?? 3, description: `[From daily plan] ${p.action}` });
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
catch { /* non-fatal */ }
|
|
@@ -132,8 +141,56 @@ export function registerSessionTools(server) {
|
|
|
132
141
|
const topItems = items.slice(0, maxItems);
|
|
133
142
|
if (topItems.length === 0)
|
|
134
143
|
return textResult('No work items discovered. All goals on track, no failures, inbox clear.');
|
|
135
|
-
|
|
144
|
+
// Decisions here are advisory — the agent surveys, then chooses whether
|
|
145
|
+
// to act. We deliberately do NOT recordDecision: the autonomous paths
|
|
146
|
+
// (processInbox, daily-plan loop, goal advancement) record their own
|
|
147
|
+
// decisions when they actually act, so the ledger reflects committed
|
|
148
|
+
// decisions and proactive_stats stays signal-rich.
|
|
149
|
+
const lines = topItems.map((i) => {
|
|
150
|
+
const decision = decideDiscoveredWorkItem(i);
|
|
151
|
+
const label = ACTION_LABEL[decision.action];
|
|
152
|
+
return `- [${i.type}] Urgency ${i.urgency}/5 → ${label}: ${i.description}`;
|
|
153
|
+
});
|
|
136
154
|
return textResult(`## Discovered Work Items (${topItems.length})\n${lines.join('\n')}`);
|
|
137
155
|
});
|
|
156
|
+
server.tool('proactive_stats', 'Report how the proactive engine has been deciding and what those decisions led to. Use to tune urgency thresholds.', {
|
|
157
|
+
since_hours: z.number().optional().describe('Window in hours (default: 168 = 7 days)'),
|
|
158
|
+
}, async ({ since_hours }) => {
|
|
159
|
+
const windowMs = (since_hours ?? 168) * 60 * 60 * 1000;
|
|
160
|
+
const records = recentDecisions({ sinceMs: windowMs }, undefined);
|
|
161
|
+
if (records.length === 0)
|
|
162
|
+
return textResult('No proactive decisions recorded in window.');
|
|
163
|
+
const bySource = new Map();
|
|
164
|
+
const outcomeByKey = new Map();
|
|
165
|
+
for (const r of records) {
|
|
166
|
+
if (r.outcome)
|
|
167
|
+
outcomeByKey.set(r.decision.idempotencyKey, r.outcome.status);
|
|
168
|
+
}
|
|
169
|
+
for (const r of records) {
|
|
170
|
+
if (r.outcome)
|
|
171
|
+
continue;
|
|
172
|
+
const sourceMap = bySource.get(r.decision.source) ?? new Map();
|
|
173
|
+
const bucket = sourceMap.get(r.decision.action) ?? { decided: 0, outcomes: {} };
|
|
174
|
+
bucket.decided++;
|
|
175
|
+
const outcome = outcomeByKey.get(r.decision.idempotencyKey);
|
|
176
|
+
if (outcome)
|
|
177
|
+
bucket.outcomes[outcome] = (bucket.outcomes[outcome] ?? 0) + 1;
|
|
178
|
+
sourceMap.set(r.decision.action, bucket);
|
|
179
|
+
bySource.set(r.decision.source, sourceMap);
|
|
180
|
+
}
|
|
181
|
+
const lines = [`## Proactive Stats (last ${since_hours ?? 168}h, ${records.length} records)\n`];
|
|
182
|
+
for (const [source, actionMap] of bySource) {
|
|
183
|
+
lines.push(`### ${source}`);
|
|
184
|
+
for (const [action, bucket] of actionMap) {
|
|
185
|
+
const outcomes = Object.entries(bucket.outcomes);
|
|
186
|
+
const outcomeStr = outcomes.length === 0
|
|
187
|
+
? 'no outcomes recorded'
|
|
188
|
+
: outcomes.map(([k, v]) => `${k}=${v}`).join(', ');
|
|
189
|
+
lines.push(`- ${ACTION_LABEL[action]} (${action}): decided ${bucket.decided}× → ${outcomeStr}`);
|
|
190
|
+
}
|
|
191
|
+
lines.push('');
|
|
192
|
+
}
|
|
193
|
+
return textResult(lines.join('\n'));
|
|
194
|
+
});
|
|
138
195
|
}
|
|
139
196
|
//# sourceMappingURL=session-tools.js.map
|
package/dist/types.d.ts
CHANGED
|
@@ -210,6 +210,7 @@ export interface HeartbeatWorkItem {
|
|
|
210
210
|
description: string;
|
|
211
211
|
prompt: string;
|
|
212
212
|
source: string;
|
|
213
|
+
idempotencyKey?: string;
|
|
213
214
|
priority: 'high' | 'normal';
|
|
214
215
|
queuedAt: string;
|
|
215
216
|
maxTurns: number;
|
|
@@ -220,6 +221,19 @@ export interface HeartbeatWorkItem {
|
|
|
220
221
|
error?: string;
|
|
221
222
|
agentSlug?: string;
|
|
222
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* State for one specialist agent's heartbeat scheduler. Persisted at
|
|
226
|
+
* ~/.clementine/heartbeat/agents/<slug>/state.json. Manager reads
|
|
227
|
+
* `nextCheckAt` to decide whether the agent is due for a tick.
|
|
228
|
+
*/
|
|
229
|
+
export interface AgentHeartbeatState {
|
|
230
|
+
slug: string;
|
|
231
|
+
lastTickAt: string;
|
|
232
|
+
nextCheckAt: string;
|
|
233
|
+
silentTickCount: number;
|
|
234
|
+
fingerprint: string;
|
|
235
|
+
lastSignalSummary?: string;
|
|
236
|
+
}
|
|
223
237
|
export interface CronJobDefinition {
|
|
224
238
|
name: string;
|
|
225
239
|
schedule: string;
|