clementine-agent 1.18.183 → 1.18.185
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/assistant.js +15 -2
- package/dist/agent/chat-stop-hook.d.ts +76 -0
- package/dist/agent/chat-stop-hook.js +155 -0
- package/dist/agent/clementine-turn-context.d.ts +102 -0
- package/dist/agent/clementine-turn-context.js +210 -0
- package/dist/agent/run-agent-context.js +35 -0
- package/dist/agent/run-agent.js +50 -1
- package/dist/agent/tool-call-dedup.js +25 -7
- package/dist/cli/dashboard.js +410 -4
- package/dist/gateway/cron-scheduler.js +6 -1
- package/dist/gateway/router.d.ts +11 -0
- package/dist/gateway/router.js +114 -2
- package/dist/memory/skill-quality.d.ts +16 -5
- package/dist/memory/skill-quality.js +37 -3
- package/package.json +1 -1
package/dist/agent/assistant.js
CHANGED
|
@@ -1025,6 +1025,21 @@ export class PersonalAssistant {
|
|
|
1025
1025
|
}
|
|
1026
1026
|
}
|
|
1027
1027
|
// ── System Prompt Builder ─────────────────────────────────────────
|
|
1028
|
+
//
|
|
1029
|
+
// 1.18.184 caveat: the canonical CHAT path no longer reaches this
|
|
1030
|
+
// builder. Chat goes through runAgent (src/agent/run-agent.ts:679)
|
|
1031
|
+
// with `systemPrompt: { type: 'preset', preset: 'claude_code',
|
|
1032
|
+
// append: <buildChatSystemAppend(...)> }`. The recall + trust posture
|
|
1033
|
+
// directives live in `run-agent-context.ts:BEHAVIORAL_POSTURE`.
|
|
1034
|
+
//
|
|
1035
|
+
// This buildSystemPrompt is now invoked only from:
|
|
1036
|
+
// - plan-step path (`processPlanStep` ~line 3396)
|
|
1037
|
+
// - auto-memory extraction Haiku passes (~line 3187)
|
|
1038
|
+
// - cron-reflection Haiku passes
|
|
1039
|
+
//
|
|
1040
|
+
// If you're adding chat-time behavioral guidance, add it to
|
|
1041
|
+
// BEHAVIORAL_POSTURE in run-agent-context.ts. Adding it here will
|
|
1042
|
+
// NOT affect real chat — only the legacy Haiku/plan-step paths.
|
|
1028
1043
|
buildSystemPrompt(opts = {}) {
|
|
1029
1044
|
const { isHeartbeat = false, cronTier = null, retrievalContext = '', profile = null, sessionKey = null, model = null, verboseLevel, intentClassification = null, contextTier = 'full', toolsAvailable = true, composioConnectedSlugs = [] } = opts;
|
|
1030
1045
|
const isAutonomous = isHeartbeat || cronTier !== null;
|
|
@@ -1208,8 +1223,6 @@ Obsidian vault with YAML frontmatter, [[wikilinks]], #tags.
|
|
|
1208
1223
|
**Remembering:** Durable facts → memory_write(action="update_memory"). Daily context → note_take / memory_write(action="append_daily"). New person → note_create. New task → task_add.
|
|
1209
1224
|
Save important facts immediately; a background agent also extracts after each exchange.
|
|
1210
1225
|
|
|
1211
|
-
**Recalling — REQUIRED behavior:** When the user references past work you don't have in immediate context — a URL, a deployment, a file you created, a task or background job you ran, a person/project/domain name you don't have inline — call \`memory_search\` (or \`transcript_search\` for chat history) BEFORE asking the user to provide it and BEFORE replying that you have no record. Saying "I don't see any record of that" without having searched is a memory failure, not an honest answer. Background tasks, cron runs, deployments, and prior chat turns are all in the SQLite memory store with dense embeddings — semantic search will surface them even when the wording doesn't match exactly.
|
|
1212
|
-
|
|
1213
1226
|
## Self-Configuration (never tell ${owner} to edit a config file)
|
|
1214
1227
|
|
|
1215
1228
|
Clementine is self-configuring. Every credential, every integration, every tool permission can be set by calling a tool — no hand-editing.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chat-stop-hook — Stop hook that keeps chat-initiated multi-step jobs
|
|
3
|
+
* running until they finish OR the user explicitly stops them.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists (1.18.184)
|
|
6
|
+
* ──────────────────────────
|
|
7
|
+
* Before this hook, the SDK loop ended whenever the model produced
|
|
8
|
+
* a final assistant message — even when the model had clearly stated
|
|
9
|
+
* "next, I'll do X" but then stopped. From the user's POV: "I asked her
|
|
10
|
+
* to draft 3 emails and she only drafted 1, no explanation." The model
|
|
11
|
+
* was being prematurely terminated by the SDK's default Stop behavior.
|
|
12
|
+
*
|
|
13
|
+
* The canonical SDK pattern for long-running agentic loops is a Stop
|
|
14
|
+
* hook that:
|
|
15
|
+
* 1. Detects when the model said it would continue but didn't.
|
|
16
|
+
* 2. Returns `decision: 'block'` with a `reason` that re-prompts
|
|
17
|
+
* the model to keep going.
|
|
18
|
+
* 3. NEVER blocks if Stop hooks have already fired this run
|
|
19
|
+
* (`input.stop_hook_active === true`) — that's the SDK's
|
|
20
|
+
* anti-infinite-loop guardrail and we honor it.
|
|
21
|
+
* 4. NEVER blocks if the user has aborted (abortSignal fired) —
|
|
22
|
+
* user intent always wins.
|
|
23
|
+
*
|
|
24
|
+
* What this hook does NOT do
|
|
25
|
+
* ──────────────────────────
|
|
26
|
+
* It does NOT force every chat turn to keep going. The default path
|
|
27
|
+
* is to LET THE MODEL FINISH. The hook only intervenes when:
|
|
28
|
+
* (a) the last assistant message contains a clear "more work to do"
|
|
29
|
+
* signal (e.g., "next, I'll", "step 2:", "I'll continue with"), AND
|
|
30
|
+
* (b) the user has NOT issued a stop / cancel, AND
|
|
31
|
+
* (c) we haven't already re-blocked this run.
|
|
32
|
+
*
|
|
33
|
+
* Conservative by design: better to let one job finish slightly short
|
|
34
|
+
* than to spin forever. If a job needs to run long, the user can
|
|
35
|
+
* always re-ask.
|
|
36
|
+
*
|
|
37
|
+
* Aligned with Anthropic SDK best practices: Stop hooks fire even
|
|
38
|
+
* under `bypassPermissions`, which is the canonical lever for
|
|
39
|
+
* "agentic loop that keeps going." See `sdk.d.ts:5483-5492` for the
|
|
40
|
+
* `StopHookInput` shape including the `stop_hook_active` guard.
|
|
41
|
+
*/
|
|
42
|
+
import type { HookCallbackMatcher, HookEvent } from '@anthropic-ai/claude-agent-sdk';
|
|
43
|
+
export interface StopHookOptions {
|
|
44
|
+
/** Stable run identifier for telemetry. */
|
|
45
|
+
runId: string;
|
|
46
|
+
/** Optional abort signal to honor — if it fires, the hook will
|
|
47
|
+
* never re-block. User-initiated stops always win. */
|
|
48
|
+
abortSignal?: AbortSignal;
|
|
49
|
+
/** Optional callback fired on every decision. Useful for the
|
|
50
|
+
* dashboard "What Clementine sees this turn" panel. */
|
|
51
|
+
onDecision?: (info: {
|
|
52
|
+
decision: 'pass' | 'continue';
|
|
53
|
+
reason?: string;
|
|
54
|
+
lastMessagePreview: string;
|
|
55
|
+
stopHookActive: boolean;
|
|
56
|
+
}) => void;
|
|
57
|
+
}
|
|
58
|
+
export interface StopHookStats {
|
|
59
|
+
/** Total Stop events inspected. */
|
|
60
|
+
inspected: number;
|
|
61
|
+
/** Stop events that passed through (model finished cleanly). */
|
|
62
|
+
passed: number;
|
|
63
|
+
/** Stop events where we re-prompted the model to continue. */
|
|
64
|
+
continued: number;
|
|
65
|
+
}
|
|
66
|
+
export interface StopHookHandles {
|
|
67
|
+
/** Hook map suitable for SDK `query({ options: { hooks } })`. */
|
|
68
|
+
hooks: Partial<Record<HookEvent, HookCallbackMatcher[]>>;
|
|
69
|
+
/** Aggregated telemetry — read after the run completes. */
|
|
70
|
+
stats: StopHookStats;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build a Stop hook for a chat-initiated agentic run.
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildChatStopHook(opts: StopHookOptions): StopHookHandles;
|
|
76
|
+
//# sourceMappingURL=chat-stop-hook.d.ts.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chat-stop-hook — Stop hook that keeps chat-initiated multi-step jobs
|
|
3
|
+
* running until they finish OR the user explicitly stops them.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists (1.18.184)
|
|
6
|
+
* ──────────────────────────
|
|
7
|
+
* Before this hook, the SDK loop ended whenever the model produced
|
|
8
|
+
* a final assistant message — even when the model had clearly stated
|
|
9
|
+
* "next, I'll do X" but then stopped. From the user's POV: "I asked her
|
|
10
|
+
* to draft 3 emails and she only drafted 1, no explanation." The model
|
|
11
|
+
* was being prematurely terminated by the SDK's default Stop behavior.
|
|
12
|
+
*
|
|
13
|
+
* The canonical SDK pattern for long-running agentic loops is a Stop
|
|
14
|
+
* hook that:
|
|
15
|
+
* 1. Detects when the model said it would continue but didn't.
|
|
16
|
+
* 2. Returns `decision: 'block'` with a `reason` that re-prompts
|
|
17
|
+
* the model to keep going.
|
|
18
|
+
* 3. NEVER blocks if Stop hooks have already fired this run
|
|
19
|
+
* (`input.stop_hook_active === true`) — that's the SDK's
|
|
20
|
+
* anti-infinite-loop guardrail and we honor it.
|
|
21
|
+
* 4. NEVER blocks if the user has aborted (abortSignal fired) —
|
|
22
|
+
* user intent always wins.
|
|
23
|
+
*
|
|
24
|
+
* What this hook does NOT do
|
|
25
|
+
* ──────────────────────────
|
|
26
|
+
* It does NOT force every chat turn to keep going. The default path
|
|
27
|
+
* is to LET THE MODEL FINISH. The hook only intervenes when:
|
|
28
|
+
* (a) the last assistant message contains a clear "more work to do"
|
|
29
|
+
* signal (e.g., "next, I'll", "step 2:", "I'll continue with"), AND
|
|
30
|
+
* (b) the user has NOT issued a stop / cancel, AND
|
|
31
|
+
* (c) we haven't already re-blocked this run.
|
|
32
|
+
*
|
|
33
|
+
* Conservative by design: better to let one job finish slightly short
|
|
34
|
+
* than to spin forever. If a job needs to run long, the user can
|
|
35
|
+
* always re-ask.
|
|
36
|
+
*
|
|
37
|
+
* Aligned with Anthropic SDK best practices: Stop hooks fire even
|
|
38
|
+
* under `bypassPermissions`, which is the canonical lever for
|
|
39
|
+
* "agentic loop that keeps going." See `sdk.d.ts:5483-5492` for the
|
|
40
|
+
* `StopHookInput` shape including the `stop_hook_active` guard.
|
|
41
|
+
*/
|
|
42
|
+
import pino from 'pino';
|
|
43
|
+
const logger = pino({ name: 'clementine.chat-stop-hook' });
|
|
44
|
+
/**
|
|
45
|
+
* Phrases in the last assistant message that signal "more work to do."
|
|
46
|
+
* Conservative — we only continue when the model EXPLICITLY said it
|
|
47
|
+
* would. Vague endings ("Let me know if you need anything else.") do
|
|
48
|
+
* NOT trigger; those are clean completions.
|
|
49
|
+
*/
|
|
50
|
+
const CONTINUATION_SIGNALS = [
|
|
51
|
+
// Explicit "next step" / sequencing
|
|
52
|
+
/\bnext,?\s+(?:i'?ll|i\s+will|let'?s|i'?m\s+going\s+to)\b/i,
|
|
53
|
+
/\bstep\s+\d+:/i,
|
|
54
|
+
/\bphase\s+\d+:/i,
|
|
55
|
+
/\bi'?ll\s+(?:now|then|next)\b/i,
|
|
56
|
+
/\bi\s+will\s+(?:now|then|next)\b/i,
|
|
57
|
+
// "Continuing with" / "moving on"
|
|
58
|
+
/\bcontinuing\s+(?:with|to)\b/i,
|
|
59
|
+
/\bmoving\s+on\s+to\b/i,
|
|
60
|
+
/\bi'?ll\s+continue\s+(?:by|with)\b/i,
|
|
61
|
+
// Promised remainder of a list
|
|
62
|
+
/\b(?:second|third|fourth|fifth|remaining|rest)\s+(?:email|email\.?|draft|item|step)/i,
|
|
63
|
+
// "After this, I'll"
|
|
64
|
+
/\bafter\s+(?:this|that),?\s+i'?ll\b/i,
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* Build a Stop hook for a chat-initiated agentic run.
|
|
68
|
+
*/
|
|
69
|
+
export function buildChatStopHook(opts) {
|
|
70
|
+
const stats = { inspected: 0, passed: 0, continued: 0 };
|
|
71
|
+
const stopHook = async (input) => {
|
|
72
|
+
if (input.hook_event_name !== 'Stop')
|
|
73
|
+
return {};
|
|
74
|
+
const evt = input;
|
|
75
|
+
stats.inspected += 1;
|
|
76
|
+
const lastMsg = evt.last_assistant_message ?? '';
|
|
77
|
+
const lastMessagePreview = lastMsg.slice(0, 160).replace(/\s+/g, ' ').trim();
|
|
78
|
+
// ── Guard 1: anti-infinite-loop ───────────────────────────────
|
|
79
|
+
// stop_hook_active is true if Stop hooks have ALREADY fired this
|
|
80
|
+
// run. SDK uses this exact field to prevent us re-blocking
|
|
81
|
+
// forever. If it's set, we must pass through.
|
|
82
|
+
if (evt.stop_hook_active) {
|
|
83
|
+
stats.passed += 1;
|
|
84
|
+
logger.debug({
|
|
85
|
+
runId: opts.runId,
|
|
86
|
+
reason: 'stop_hook_active',
|
|
87
|
+
lastMessagePreview,
|
|
88
|
+
}, 'Stop hook passing — already active');
|
|
89
|
+
opts.onDecision?.({
|
|
90
|
+
decision: 'pass',
|
|
91
|
+
reason: 'stop_hook_active',
|
|
92
|
+
lastMessagePreview,
|
|
93
|
+
stopHookActive: true,
|
|
94
|
+
});
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
// ── Guard 2: user-initiated stop ──────────────────────────────
|
|
98
|
+
// If the abort signal has fired, the user wants out. Never
|
|
99
|
+
// re-block. User intent ALWAYS wins.
|
|
100
|
+
if (opts.abortSignal?.aborted) {
|
|
101
|
+
stats.passed += 1;
|
|
102
|
+
logger.debug({
|
|
103
|
+
runId: opts.runId,
|
|
104
|
+
reason: 'user_aborted',
|
|
105
|
+
lastMessagePreview,
|
|
106
|
+
}, 'Stop hook passing — user aborted');
|
|
107
|
+
opts.onDecision?.({
|
|
108
|
+
decision: 'pass',
|
|
109
|
+
reason: 'user_aborted',
|
|
110
|
+
lastMessagePreview,
|
|
111
|
+
stopHookActive: false,
|
|
112
|
+
});
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
// ── Detection: did the model say it would continue? ──────────
|
|
116
|
+
const continuationMatched = CONTINUATION_SIGNALS.some((rx) => rx.test(lastMsg));
|
|
117
|
+
if (!continuationMatched) {
|
|
118
|
+
// No continuation signal — let the model finish.
|
|
119
|
+
stats.passed += 1;
|
|
120
|
+
opts.onDecision?.({
|
|
121
|
+
decision: 'pass',
|
|
122
|
+
reason: 'clean_completion',
|
|
123
|
+
lastMessagePreview,
|
|
124
|
+
stopHookActive: false,
|
|
125
|
+
});
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
// ── Re-prompt: keep going ──────────────────────────────────────
|
|
129
|
+
stats.continued += 1;
|
|
130
|
+
const reason = 'You said you would continue with more work but the loop is about to end. ' +
|
|
131
|
+
'Keep going — finish the remaining steps you outlined. ' +
|
|
132
|
+
'If you genuinely cannot continue (waiting on external input, hit a hard error, etc.), say so explicitly in your next message so the owner knows where you stopped.';
|
|
133
|
+
logger.info({
|
|
134
|
+
runId: opts.runId,
|
|
135
|
+
lastMessagePreview,
|
|
136
|
+
}, 'Stop hook re-prompting model to continue work it announced');
|
|
137
|
+
opts.onDecision?.({
|
|
138
|
+
decision: 'continue',
|
|
139
|
+
reason,
|
|
140
|
+
lastMessagePreview,
|
|
141
|
+
stopHookActive: false,
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
decision: 'block',
|
|
145
|
+
reason,
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
hooks: {
|
|
150
|
+
Stop: [{ hooks: [stopHook] }],
|
|
151
|
+
},
|
|
152
|
+
stats,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=chat-stop-hook.js.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* clementine-turn-context — the volatile per-turn context block that
|
|
3
|
+
* reconstitutes Clementine's live awareness on every chat turn.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists (1.18.184)
|
|
6
|
+
* ──────────────────────────
|
|
7
|
+
* The modern chat path's system prompt is the SDK's `claude_code`
|
|
8
|
+
* preset + `buildChatSystemAppend()` (run-agent-context.ts). That gives
|
|
9
|
+
* Clementine her identity (SOUL) and her hand-curated long-term memory
|
|
10
|
+
* (MEMORY.md), but it is STATIC across turns by design — anything that
|
|
11
|
+
* varies per turn must NOT live there or it would invalidate the
|
|
12
|
+
* Anthropic prompt cache.
|
|
13
|
+
*
|
|
14
|
+
* Anything volatile — what's true right now, what just happened, what
|
|
15
|
+
* the SQLite memory store has relevant to the current message — lives
|
|
16
|
+
* here, in a block prepended to the user's message. The SDK treats
|
|
17
|
+
* that as turn input, not as system prompt, so it doesn't break cache
|
|
18
|
+
* and it gives the model fresh context every turn.
|
|
19
|
+
*
|
|
20
|
+
* What we put in the block
|
|
21
|
+
* ────────────────────────
|
|
22
|
+
* 1. Retrieved memory hits from the SQLite store (semantic + FTS),
|
|
23
|
+
* scored against the user's current message. The single highest-
|
|
24
|
+
* leverage section — this is how persistent memory of EVERYTHING
|
|
25
|
+
* actually reaches the model.
|
|
26
|
+
* 2. Recent background-task headlines (last 24h, terminal status only)
|
|
27
|
+
* so the model knows what work just completed without re-asking.
|
|
28
|
+
* 3. Live state — current date/time + channel/identity framing.
|
|
29
|
+
* 4. Extension points for the deeper learning subsystems (decision-
|
|
30
|
+
* reflection, skill-quality, insight-engine, seed-user-model,
|
|
31
|
+
* goal-evaluator) — each one is a labeled section that returns
|
|
32
|
+
* empty today and can be wired in a follow-up ship without
|
|
33
|
+
* re-architecting.
|
|
34
|
+
*
|
|
35
|
+
* Hard cap on total block size — see MAX_BLOCK_CHARS. Anthropic's prompt
|
|
36
|
+
* cache benefit dies if the volatile block is larger than the cacheable
|
|
37
|
+
* prefix, so keep this tight.
|
|
38
|
+
*
|
|
39
|
+
* Aligned with Anthropic SDK best practices: per-turn dynamic context
|
|
40
|
+
* in the USER message, NOT in the system prompt. See the SDK reference
|
|
41
|
+
* note on prompt caching boundaries.
|
|
42
|
+
*/
|
|
43
|
+
import type { BackgroundTask } from '../types.js';
|
|
44
|
+
export interface BuildTurnContextOptions {
|
|
45
|
+
/** The user's current message — used as the query for retrieved memory. */
|
|
46
|
+
userMessage: string;
|
|
47
|
+
/** Session key for the active chat. Used for log breadcrumbs and to
|
|
48
|
+
* scope future per-session reads (currently unused; searchContext
|
|
49
|
+
* is intentionally cross-session for single-owner installs). */
|
|
50
|
+
sessionKey: string;
|
|
51
|
+
/** Where the user is reaching Clementine from. Surfaces in the
|
|
52
|
+
* identity framing block. Examples: "discord-dm", "dashboard",
|
|
53
|
+
* "slack-channel", "chat". */
|
|
54
|
+
channel?: string;
|
|
55
|
+
/** Owner-facing name (display name, not slug). When set, used in
|
|
56
|
+
* the identity framing block. */
|
|
57
|
+
ownerName?: string | null;
|
|
58
|
+
/** Active hired-agent profile if running as one. Affects the
|
|
59
|
+
* identity framing — "you are talking to Sasha right now," not
|
|
60
|
+
* "you are Clementine". */
|
|
61
|
+
profileName?: string | null;
|
|
62
|
+
/** Read-only memory store handle. When absent, retrieved-memory
|
|
63
|
+
* section is skipped — the rest still renders. */
|
|
64
|
+
memoryStore?: {
|
|
65
|
+
searchContext?: (query: string, opts?: {
|
|
66
|
+
limit?: number;
|
|
67
|
+
}) => Array<{
|
|
68
|
+
source_file?: string;
|
|
69
|
+
section?: string;
|
|
70
|
+
content: string;
|
|
71
|
+
score?: number;
|
|
72
|
+
}>;
|
|
73
|
+
} | null;
|
|
74
|
+
/** Optional override: synchronous read of recent terminal-state bg
|
|
75
|
+
* tasks. Defaults to a one-time module-cached listBackgroundTasks
|
|
76
|
+
* import (lazy, since not all callers have one). */
|
|
77
|
+
listBackgroundTasks?: (filter: {
|
|
78
|
+
status?: BackgroundTask['status'];
|
|
79
|
+
}) => BackgroundTask[];
|
|
80
|
+
/** Clock injection for tests. Defaults to Date.now(). */
|
|
81
|
+
now?: () => number;
|
|
82
|
+
}
|
|
83
|
+
export interface BuildTurnContextResult {
|
|
84
|
+
/** The full ready-to-prepend context block, INCLUDING outer
|
|
85
|
+
* `[Context...]\n...\n[/Context]\n\n` fence. Empty string when no
|
|
86
|
+
* sections produced output (e.g., builder sessions or completely
|
|
87
|
+
* empty stores) — caller can treat empty as "no prefix needed". */
|
|
88
|
+
block: string;
|
|
89
|
+
/** Telemetry — which sections contributed, for the dashboard
|
|
90
|
+
* "what Clementine sees this turn" panel. */
|
|
91
|
+
sections: {
|
|
92
|
+
retrievedMemory: number;
|
|
93
|
+
recentBgTasks: number;
|
|
94
|
+
liveState: boolean;
|
|
95
|
+
identityFrame: boolean;
|
|
96
|
+
};
|
|
97
|
+
/** Final character count of the block. Useful for logging + the
|
|
98
|
+
* Anthropic prompt-cache-health analysis. */
|
|
99
|
+
totalChars: number;
|
|
100
|
+
}
|
|
101
|
+
export declare function buildClementineTurnContext(opts: BuildTurnContextOptions): BuildTurnContextResult;
|
|
102
|
+
//# sourceMappingURL=clementine-turn-context.d.ts.map
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* clementine-turn-context — the volatile per-turn context block that
|
|
3
|
+
* reconstitutes Clementine's live awareness on every chat turn.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists (1.18.184)
|
|
6
|
+
* ──────────────────────────
|
|
7
|
+
* The modern chat path's system prompt is the SDK's `claude_code`
|
|
8
|
+
* preset + `buildChatSystemAppend()` (run-agent-context.ts). That gives
|
|
9
|
+
* Clementine her identity (SOUL) and her hand-curated long-term memory
|
|
10
|
+
* (MEMORY.md), but it is STATIC across turns by design — anything that
|
|
11
|
+
* varies per turn must NOT live there or it would invalidate the
|
|
12
|
+
* Anthropic prompt cache.
|
|
13
|
+
*
|
|
14
|
+
* Anything volatile — what's true right now, what just happened, what
|
|
15
|
+
* the SQLite memory store has relevant to the current message — lives
|
|
16
|
+
* here, in a block prepended to the user's message. The SDK treats
|
|
17
|
+
* that as turn input, not as system prompt, so it doesn't break cache
|
|
18
|
+
* and it gives the model fresh context every turn.
|
|
19
|
+
*
|
|
20
|
+
* What we put in the block
|
|
21
|
+
* ────────────────────────
|
|
22
|
+
* 1. Retrieved memory hits from the SQLite store (semantic + FTS),
|
|
23
|
+
* scored against the user's current message. The single highest-
|
|
24
|
+
* leverage section — this is how persistent memory of EVERYTHING
|
|
25
|
+
* actually reaches the model.
|
|
26
|
+
* 2. Recent background-task headlines (last 24h, terminal status only)
|
|
27
|
+
* so the model knows what work just completed without re-asking.
|
|
28
|
+
* 3. Live state — current date/time + channel/identity framing.
|
|
29
|
+
* 4. Extension points for the deeper learning subsystems (decision-
|
|
30
|
+
* reflection, skill-quality, insight-engine, seed-user-model,
|
|
31
|
+
* goal-evaluator) — each one is a labeled section that returns
|
|
32
|
+
* empty today and can be wired in a follow-up ship without
|
|
33
|
+
* re-architecting.
|
|
34
|
+
*
|
|
35
|
+
* Hard cap on total block size — see MAX_BLOCK_CHARS. Anthropic's prompt
|
|
36
|
+
* cache benefit dies if the volatile block is larger than the cacheable
|
|
37
|
+
* prefix, so keep this tight.
|
|
38
|
+
*
|
|
39
|
+
* Aligned with Anthropic SDK best practices: per-turn dynamic context
|
|
40
|
+
* in the USER message, NOT in the system prompt. See the SDK reference
|
|
41
|
+
* note on prompt caching boundaries.
|
|
42
|
+
*/
|
|
43
|
+
import pino from 'pino';
|
|
44
|
+
const logger = pino({ name: 'clementine.turn-context' });
|
|
45
|
+
// ── Tunables ──────────────────────────────────────────────────────────
|
|
46
|
+
/** Hard cap on the entire block. Keep volatile content small so the
|
|
47
|
+
* cacheable prefix stays larger than the dynamic delta. */
|
|
48
|
+
const MAX_BLOCK_CHARS = 4_000;
|
|
49
|
+
/** Per-section caps so any one section can't crowd out the others. */
|
|
50
|
+
const MAX_MEMORY_HITS = 6;
|
|
51
|
+
const MAX_MEMORY_HIT_CHARS = 320;
|
|
52
|
+
const MAX_BG_TASKS = 3;
|
|
53
|
+
const MAX_BG_TASK_LINE_CHARS = 200;
|
|
54
|
+
const RECENT_BG_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
55
|
+
// ── The builder ───────────────────────────────────────────────────────
|
|
56
|
+
export function buildClementineTurnContext(opts) {
|
|
57
|
+
const sections = {
|
|
58
|
+
retrievedMemory: 0,
|
|
59
|
+
recentBgTasks: 0,
|
|
60
|
+
liveState: false,
|
|
61
|
+
identityFrame: false,
|
|
62
|
+
};
|
|
63
|
+
const parts = [];
|
|
64
|
+
const nowMs = (opts.now ?? Date.now)();
|
|
65
|
+
const nowDate = new Date(nowMs);
|
|
66
|
+
// ── 1. Retrieved memory hits ──────────────────────────────────────
|
|
67
|
+
// The single most important section. Pulls the top semantic + FTS
|
|
68
|
+
// hits from the SQLite memory store, scored against the user's
|
|
69
|
+
// current message. Without this, Clementine has no automatic recall
|
|
70
|
+
// — she'd have to spontaneously call memory_search every turn.
|
|
71
|
+
if (opts.memoryStore?.searchContext && opts.userMessage.trim().length > 0) {
|
|
72
|
+
try {
|
|
73
|
+
const hits = opts.memoryStore.searchContext(opts.userMessage, {
|
|
74
|
+
limit: MAX_MEMORY_HITS,
|
|
75
|
+
});
|
|
76
|
+
if (hits && hits.length > 0) {
|
|
77
|
+
const lines = ['### Possibly relevant from persistent memory'];
|
|
78
|
+
for (const h of hits.slice(0, MAX_MEMORY_HITS)) {
|
|
79
|
+
const label = h.section
|
|
80
|
+
? h.section
|
|
81
|
+
: (h.source_file ? h.source_file.split('/').pop() ?? h.source_file : 'memory');
|
|
82
|
+
const content = (h.content ?? '').slice(0, MAX_MEMORY_HIT_CHARS).trim();
|
|
83
|
+
if (!content)
|
|
84
|
+
continue;
|
|
85
|
+
lines.push(`- **${label}**: ${content}`);
|
|
86
|
+
sections.retrievedMemory += 1;
|
|
87
|
+
}
|
|
88
|
+
if (sections.retrievedMemory > 0) {
|
|
89
|
+
parts.push(lines.join('\n'));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
// Never block on memory failure — log and continue.
|
|
95
|
+
logger.debug({ err, sessionKey: opts.sessionKey }, 'turn-context: searchContext failed (non-fatal)');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// ── 2. Recent background task headlines ───────────────────────────
|
|
99
|
+
// Last 24h of terminal-state bg tasks. So when the owner asks "what
|
|
100
|
+
// happened with that job?" she knows without re-asking.
|
|
101
|
+
if (opts.listBackgroundTasks) {
|
|
102
|
+
try {
|
|
103
|
+
const TERMINAL = ['done', 'failed', 'interrupted', 'aborted'];
|
|
104
|
+
const recent = [];
|
|
105
|
+
for (const status of TERMINAL) {
|
|
106
|
+
const tasks = opts.listBackgroundTasks({ status });
|
|
107
|
+
for (const task of tasks) {
|
|
108
|
+
const stamp = task.completedAt ?? task.interruptedAt ?? task.startedAt ?? task.createdAt;
|
|
109
|
+
if (!stamp)
|
|
110
|
+
continue;
|
|
111
|
+
if (nowMs - Date.parse(stamp) > RECENT_BG_WINDOW_MS)
|
|
112
|
+
continue;
|
|
113
|
+
recent.push(task);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Newest first, capped.
|
|
117
|
+
recent.sort((a, b) => {
|
|
118
|
+
const aStamp = a.completedAt ?? a.startedAt ?? a.createdAt ?? '';
|
|
119
|
+
const bStamp = b.completedAt ?? b.startedAt ?? b.createdAt ?? '';
|
|
120
|
+
return bStamp.localeCompare(aStamp);
|
|
121
|
+
});
|
|
122
|
+
if (recent.length > 0) {
|
|
123
|
+
const lines = ['### Recently completed background work (last 24h)'];
|
|
124
|
+
for (const task of recent.slice(0, MAX_BG_TASKS)) {
|
|
125
|
+
const promptPreview = (task.prompt ?? '').slice(0, 80).replace(/\s+/g, ' ').trim();
|
|
126
|
+
const tail = task.status === 'done'
|
|
127
|
+
? (task.result ?? task.deliverableNote ?? 'done').slice(0, 100).replace(/\s+/g, ' ').trim()
|
|
128
|
+
: (task.error ?? task.status).slice(0, 100).replace(/\s+/g, ' ').trim();
|
|
129
|
+
const line = `- **${task.status}**: ${promptPreview} → ${tail}`;
|
|
130
|
+
lines.push(line.slice(0, MAX_BG_TASK_LINE_CHARS));
|
|
131
|
+
sections.recentBgTasks += 1;
|
|
132
|
+
}
|
|
133
|
+
if (sections.recentBgTasks > 0) {
|
|
134
|
+
parts.push(lines.join('\n'));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
logger.debug({ err, sessionKey: opts.sessionKey }, 'turn-context: listBackgroundTasks failed (non-fatal)');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// ── 3. Identity framing ───────────────────────────────────────────
|
|
143
|
+
// "Who is the user, where are they reaching you, which agent are
|
|
144
|
+
// you running as." Anchors the model's voice + addressing.
|
|
145
|
+
const identityLine = buildIdentityLine(opts);
|
|
146
|
+
if (identityLine) {
|
|
147
|
+
parts.push(`### Right now\n${identityLine}`);
|
|
148
|
+
sections.identityFrame = true;
|
|
149
|
+
}
|
|
150
|
+
// ── 4. Live state ─────────────────────────────────────────────────
|
|
151
|
+
// Current date/time so the model never says "I don't know what
|
|
152
|
+
// today is." Cheap, high-signal.
|
|
153
|
+
const liveLine = `Current time: ${nowDate.toISOString()} (UTC)`;
|
|
154
|
+
if (sections.identityFrame) {
|
|
155
|
+
// Fold into the same "Right now" section to avoid an extra header.
|
|
156
|
+
parts[parts.length - 1] = `${parts[parts.length - 1]}\n${liveLine}`;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
parts.push(`### Right now\n${liveLine}`);
|
|
160
|
+
}
|
|
161
|
+
sections.liveState = true;
|
|
162
|
+
// ── 5. Extension points for deeper learning subsystems ───────────
|
|
163
|
+
// These are intentionally empty today. The architecture is set up
|
|
164
|
+
// so adding a new subsystem = adding a new builder function +
|
|
165
|
+
// calling it here. Each follow-up ship can wire one at a time
|
|
166
|
+
// without re-touching the rest of the module.
|
|
167
|
+
//
|
|
168
|
+
// TODO(1.18.185+): wire these in:
|
|
169
|
+
// - decision-reflection: latest formatReflectionSummary() if <24h old
|
|
170
|
+
// - skill-quality: skills flagged 'underperforming' or 'stale'
|
|
171
|
+
// - insight-engine: most recent generated insights not yet ack'd
|
|
172
|
+
// - seed-user-model: latest persisted snapshot of the owner profile
|
|
173
|
+
// - goal-evaluator: active goals and last 3 goal-check results
|
|
174
|
+
//
|
|
175
|
+
// For each, the pattern is: read fast, cap output, log non-fatally on error.
|
|
176
|
+
if (parts.length === 0) {
|
|
177
|
+
return { block: '', sections, totalChars: 0 };
|
|
178
|
+
}
|
|
179
|
+
const body = parts.join('\n\n');
|
|
180
|
+
// Hard cap on the whole block to protect cache health.
|
|
181
|
+
const truncated = body.length > MAX_BLOCK_CHARS
|
|
182
|
+
? body.slice(0, MAX_BLOCK_CHARS - 3) + '...'
|
|
183
|
+
: body;
|
|
184
|
+
// Mirror the existing securityAnnotation envelope shape so the chat
|
|
185
|
+
// path can concatenate cleanly.
|
|
186
|
+
const block = `[Context — read this for continuity, then respond to the user message below]\n${truncated}\n[/Context]\n\n`;
|
|
187
|
+
return {
|
|
188
|
+
block,
|
|
189
|
+
sections,
|
|
190
|
+
totalChars: block.length,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
194
|
+
function buildIdentityLine(opts) {
|
|
195
|
+
const parts = [];
|
|
196
|
+
if (opts.ownerName) {
|
|
197
|
+
parts.push(`You're talking to ${opts.ownerName}`);
|
|
198
|
+
}
|
|
199
|
+
if (opts.channel) {
|
|
200
|
+
parts.push(`via ${opts.channel}`);
|
|
201
|
+
}
|
|
202
|
+
if (opts.profileName) {
|
|
203
|
+
parts.push(`as ${opts.profileName}`);
|
|
204
|
+
}
|
|
205
|
+
else if (parts.length > 0) {
|
|
206
|
+
parts.push('as Clementine');
|
|
207
|
+
}
|
|
208
|
+
return parts.length > 0 ? parts.join(' ') + '.' : '';
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=clementine-turn-context.js.map
|
|
@@ -81,8 +81,43 @@ export function buildChatSystemAppend(opts = {}) {
|
|
|
81
81
|
if (opts.profileAppend?.trim()) {
|
|
82
82
|
blocks.push(opts.profileAppend);
|
|
83
83
|
}
|
|
84
|
+
// 5. Behavioral posture (1.18.184) — re-anchored from the legacy
|
|
85
|
+
// assistant.ts:buildSystemPrompt path which the modern chat no
|
|
86
|
+
// longer goes through. Kept in the cacheable system-prompt append
|
|
87
|
+
// (NOT in the per-turn user-message context) so Anthropic's
|
|
88
|
+
// prompt cache holds these directives across turns. They are
|
|
89
|
+
// short, stable, and describe HOW Clementine should behave —
|
|
90
|
+
// not WHAT is true right now (that's the volatile turn-context
|
|
91
|
+
// block; see clementine-turn-context.ts).
|
|
92
|
+
blocks.push(BEHAVIORAL_POSTURE);
|
|
84
93
|
return blocks.join('\n\n');
|
|
85
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Stable behavioral directives appended to every chat-path system prompt.
|
|
97
|
+
*
|
|
98
|
+
* Trust posture: Clementine runs on the owner's machine; she is the
|
|
99
|
+
* owner's agent, not a faceless assistant. She acts decisively because
|
|
100
|
+
* the trust relationship has already been established by virtue of the
|
|
101
|
+
* owner running her on their own hardware. Permission-prompt friction
|
|
102
|
+
* is a violation of the trust model.
|
|
103
|
+
*
|
|
104
|
+
* Recall posture (re-anchored from 1.18.181, originally at
|
|
105
|
+
* assistant.ts:1382 in the legacy buildSystemPrompt path): durable
|
|
106
|
+
* memory across all sessions / channels / bg tasks lives in SQLite with
|
|
107
|
+
* dense embeddings. The model has `memory_search` and `transcript_search`
|
|
108
|
+
* tools available. "I have no record" without first searching is a
|
|
109
|
+
* memory failure, not an honest answer.
|
|
110
|
+
*
|
|
111
|
+
* Kept short (<600 chars total) so it doesn't crowd the cacheable
|
|
112
|
+
* prefix; each directive is one sentence of guidance, not a manual.
|
|
113
|
+
*/
|
|
114
|
+
const BEHAVIORAL_POSTURE = `## How you operate
|
|
115
|
+
|
|
116
|
+
**Trust posture.** You are running on your owner's trusted local machine as their Clementine. The owner has already authenticated every integration you have access to; you act as them, with their judgment. Don't ask permission for things you have tools to do — just do them. Save permission-asking for genuinely consequential or ambiguous actions (sending the email vs drafting it, deleting vs archiving). Friction-by-default is a violation of the relationship.
|
|
117
|
+
|
|
118
|
+
**Recall posture.** Your durable memory across every session, every channel, every background task, and every workflow lives in SQLite with dense embeddings. When the owner references past work you don't have in immediate context — a URL, a deployment, a person, a project, a file you created, a job you ran — call \`memory_search\` (or \`transcript_search\` for chat-history specifics) BEFORE asking the owner to provide it and BEFORE replying that you have no record. "I don't see any record of that" without having searched is a memory failure, not an honest answer.
|
|
119
|
+
|
|
120
|
+
**Persistence posture.** When the owner gives you a multi-step job in chat, run it to completion. If you hit a real constraint (budget, cap, missing input, validation needed), say so explicitly — never trail off silently. The owner can always stop you via cancel or by typing \`stop\`; that's their lever. Yours is to keep going until the work is done.`;
|
|
86
121
|
/**
|
|
87
122
|
* Read the long-term memory block for an autonomous run (cron, team-task).
|
|
88
123
|
* Returns the agent-specific MEMORY.md when a hired agent is active, the
|