clementine-agent 1.18.54 → 1.18.56

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.
@@ -18,6 +18,7 @@ import cron from 'node-cron';
18
18
  import { TunnelManager } from './tunnel.js';
19
19
  import { AgentManager } from '../agent/agent-manager.js';
20
20
  import { discoverMcpServers, getClaudeIntegrations } from '../agent/mcp-bridge.js';
21
+ import { buildBuilderEnrichedMessage, builderSessionKey } from '../dashboard/builder/prompt.js';
21
22
  import { AGENTS_DIR, SESSIONS_FILE, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
22
23
  import { parseTasks } from '../tools/shared.js';
23
24
  import { todayISO } from '../gateway/cron-scheduler.js';
@@ -8019,100 +8020,29 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
8019
8020
  }
8020
8021
  });
8021
8022
  // ── Builder chat endpoint ──────────────────────────────────────────
8022
- // Track which builder sessions have received the full system prefix
8023
- const builderSessionInited = new Set();
8024
8023
  app.post('/api/builder/chat', async (req, res) => {
8025
8024
  const { message, artifactType, agentSlug, currentArtifact, attachments, linkedTools } = req.body;
8026
8025
  if (!message || typeof message !== 'string') {
8027
8026
  res.status(400).json({ error: 'message is required' });
8028
8027
  return;
8029
8028
  }
8030
- const type = artifactType || 'skill';
8031
- const sessionKey = `dashboard:builder:${type}:${agentSlug || 'clementine'}`;
8032
- const isFirstMessage = !builderSessionInited.has(sessionKey);
8033
- // ── Artifact state (compact JSON — no pretty-print to save tokens) ──
8034
- const artifactContext = currentArtifact
8035
- ? `\n[CURRENT ARTIFACT STATE]\n\`\`\`json-artifact\n${JSON.stringify(currentArtifact)}\n\`\`\`\n`
8036
- : '';
8037
- // ── File attachments — decode base64 text files and inject contents ──
8038
- let fileContext = '';
8039
- if (Array.isArray(attachments) && attachments.length > 0) {
8040
- const fileParts = [];
8041
- for (const att of attachments) {
8042
- if (att.filename && att.content) {
8043
- try {
8044
- const decoded = Buffer.from(att.content, 'base64').toString('utf-8');
8045
- // Cap each file at 4K chars to keep context reasonable
8046
- const trimmed = decoded.length > 4000 ? decoded.slice(0, 4000) + '\n... (truncated)' : decoded;
8047
- fileParts.push(`### ${att.filename}\n\`\`\`\n${trimmed}\n\`\`\``);
8048
- }
8049
- catch { /* skip binary files */ }
8050
- }
8051
- }
8052
- if (fileParts.length > 0) {
8053
- fileContext = `\n[REFERENCE FILES — the user attached these for context]\n${fileParts.join('\n\n')}\n`;
8054
- }
8055
- }
8056
- // ── Linked tools context ──
8057
- let toolContext = '';
8058
- if (Array.isArray(linkedTools) && linkedTools.length > 0) {
8059
- toolContext = `\n[LINKED TOOLS — this skill should use these tools: ${linkedTools.join(', ')}]\n`;
8060
- }
8061
- // ── Build the enriched message ──
8062
- let enrichedMessage;
8063
- if (isFirstMessage) {
8064
- // Full system prefix on first message only
8065
- const agentContext = agentSlug ? `You are building this for the agent "${agentSlug}". The skill/cron will be scoped to this agent specifically.\n` : '';
8066
- const builderPrefix = type === 'skill'
8067
- ? `[BUILDER MODE: You are helping build a reusable skill. ${agentContext}As you develop the procedure, output the current state as a JSON block:\n` +
8068
- '```json-artifact\n{"type":"skill","title":"...","description":"...","triggers":["..."],"steps":"markdown procedure","toolsUsed":["tool1","tool2"]}\n```\n' +
8069
- `Update this block in EVERY response as the skill evolves. If the user has linked tools, include them in the toolsUsed array. Ask clarifying questions to refine the procedure. Keep it conversational — one question at a time. ` +
8070
- `When the user says "save" or approves, output the final artifact block.]\n\n`
8071
- : type === 'cron'
8072
- ? `[BUILDER MODE: You are helping build a scheduled cron job. As you develop the job, output the current state as a JSON block:\n` +
8073
- '```json-artifact\n{"type":"cron","name":"...","schedule":"cron expression","tier":1,"prompt":"the full job prompt","mode":"standard","enabled":true}\n```\n' +
8074
- `Update this block in EVERY response as the job evolves. Ask about schedule, what it should do, which tools/APIs it needs, what tier (1=read-only, 2=read-write), and whether it should run in unleashed mode.\n` +
8075
- `IMPORTANT: Cron jobs automatically pull in matching skills (learned procedures) at runtime. If the user describes a workflow that should be reusable, suggest creating it as a skill first, then building the cron job that references those trigger keywords. This way the cron gets smarter over time as skills improve.\n` +
8076
- `When the user says "save" or approves, output the final artifact block.]\n\n`
8077
- : type === 'agent'
8078
- ? `[BUILDER MODE: You are helping create a new AI agent team member. As you develop the agent config, output the current state as a JSON block:\n` +
8079
- '```json-artifact\n{"type":"agent","name":"...","description":"role description","model":"sonnet","personality":"system prompt / onboarding brief","tools":["tool1","tool2"],"channel":"","tier":2}\n```\n' +
8080
- `Update this block in EVERY response as the agent evolves. Ask about: the agent's role, what tools it needs, what model to use (haiku/sonnet/opus), its personality/system prompt, which channel it should operate in, and its security tier.\n` +
8081
- `Help the user think about what makes a good agent: clear role, specific tools, focused personality. Keep it conversational — one question at a time.\n` +
8082
- `When the user says "save" or approves, output the final artifact block.]\n\n`
8083
- : type === 'workflow'
8084
- ? `[BUILDER MODE: You are helping the user DRAFT a "trick" — a (possibly multi-step) thing Clementine can do on a schedule or on demand. You are NOT executing the trick. You are not running anything in the background. You are only authoring a spec the user will save, then run later from the dashboard.\n` +
8085
- `\n` +
8086
- `Hard rules:\n` +
8087
- ` - NEVER say "on it", "running in the background", "I'll follow up", "working on it now", or anything else that implies you're executing the user's request. You are drafting a spec.\n` +
8088
- ` - Stay strictly conversational. One short question per turn. Update the artifact block on every turn.\n` +
8089
- ` - If the user describes "real work" (multi-step actions, scrapers, enrichments, reports), still just draft it — don't dispatch.\n` +
8090
- `\n` +
8091
- `As you develop the trick, output the current state as a JSON block:\n` +
8092
- '```json-artifact\n{"type":"workflow","name":"...","description":"...","schedule":"","model":"","steps":"step1:\\n prompt: ...\\nstep2:\\n prompt: ...\\n dependsOn: step1"}\n```\n' +
8093
- `Ask about (in roughly this order, one at a time):\n` +
8094
- ` 1. The goal (one sentence is fine — confirm it back).\n` +
8095
- ` 2. When it should run — natural language is fine ("every weekday at 9"); convert to a cron expression in the schedule field. Empty schedule = manual.\n` +
8096
- ` 3. Which tools, projects, or channels she'll need (MCP servers, local CLIs like sf/gh/gcloud, Slack/Discord targets).\n` +
8097
- ` 4. Which model — claude-opus-4-7 (most capable), claude-sonnet-4-6 (balanced), or claude-haiku-4-5-20251001 (fastest). Leave model empty if the user doesn't care.\n` +
8098
- `Most tricks need only one prompt step. Add steps only when the user explicitly wants a multi-step pipeline.\n` +
8099
- `When the user says "save" or approves, output the final artifact block — don't try to save it yourself, the dashboard handles persistence.]\n\n`
8100
- : `[BUILDER MODE: You are helping configure an artifact. Output structured JSON blocks as you build.]\n\n`;
8101
- enrichedMessage = builderPrefix + fileContext + toolContext + artifactContext + message;
8102
- builderSessionInited.add(sessionKey);
8103
- }
8104
- else {
8105
- // Subsequent messages: just artifact state + files + tools + user message (no repeated prefix)
8106
- enrichedMessage = fileContext + toolContext + artifactContext + message;
8107
- }
8029
+ const sessionKey = builderSessionKey(artifactType, agentSlug);
8108
8030
  try {
8109
8031
  const gateway = await getGateway();
8110
- // Builder generates JSON artifacts no tool calls. Pin the session
8111
- // toolset to 'none' so buildOptions strips all MCP servers and tool
8112
- // schemas from the system prompt. Without this, every tiny builder
8113
- // turn writes 60–280 KB of cache_creation for tool schemas the
8114
- // model never uses.
8115
- gateway.setSessionToolset(sessionKey, 'none');
8032
+ // First-message detection uses the SDK session ID set after the
8033
+ // first runAgent turn returns, persisted across daemon restarts.
8034
+ // Anchoring on this means we send the system prefix exactly once
8035
+ // per genuine new conversation, not once per process lifetime.
8036
+ const isFirstMessage = !gateway.assistant.getSdkSessionId(sessionKey);
8037
+ const enrichedMessage = buildBuilderEnrichedMessage({
8038
+ message,
8039
+ artifactType,
8040
+ agentSlug,
8041
+ currentArtifact,
8042
+ attachments,
8043
+ linkedTools,
8044
+ isFirstMessage,
8045
+ });
8116
8046
  const response = await gateway.handleMessage(sessionKey, enrichedMessage);
8117
8047
  // Parse any json-artifact blocks from the response
8118
8048
  let artifact = null;
@@ -8165,84 +8095,20 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
8165
8095
  // before the gateway warms up (otherwise some HTTP intermediaries hold
8166
8096
  // the response until first body byte).
8167
8097
  writeEvent('progress', { status: 'connecting…' });
8168
- // ── Same enrichment as /api/builder/chat (system prefix on first turn,
8169
- // artifact + files + tools on every turn). Inlined to keep the diff
8170
- // contained; refactor into a helper if a third endpoint shows up.
8171
- const type = artifactType || 'skill';
8172
- const sessionKey = `dashboard:builder:${type}:${agentSlug || 'clementine'}`;
8173
- const isFirstMessage = !builderSessionInited.has(sessionKey);
8174
- const artifactContext = currentArtifact
8175
- ? `\n[CURRENT ARTIFACT STATE]\n\`\`\`json-artifact\n${JSON.stringify(currentArtifact)}\n\`\`\`\n`
8176
- : '';
8177
- let fileContext = '';
8178
- if (Array.isArray(attachments) && attachments.length > 0) {
8179
- const fileParts = [];
8180
- for (const att of attachments) {
8181
- if (att.filename && att.content) {
8182
- try {
8183
- const decoded = Buffer.from(att.content, 'base64').toString('utf-8');
8184
- const trimmed = decoded.length > 4000 ? decoded.slice(0, 4000) + '\n... (truncated)' : decoded;
8185
- fileParts.push(`### ${att.filename}\n\`\`\`\n${trimmed}\n\`\`\``);
8186
- }
8187
- catch { /* skip binary files */ }
8188
- }
8189
- }
8190
- if (fileParts.length > 0) {
8191
- fileContext = `\n[REFERENCE FILES — the user attached these for context]\n${fileParts.join('\n\n')}\n`;
8192
- }
8193
- }
8194
- let toolContext = '';
8195
- if (Array.isArray(linkedTools) && linkedTools.length > 0) {
8196
- toolContext = `\n[LINKED TOOLS — this skill should use these tools: ${linkedTools.join(', ')}]\n`;
8197
- }
8198
- let enrichedMessage;
8199
- if (isFirstMessage) {
8200
- const agentContext = agentSlug ? `You are building this for the agent "${agentSlug}". The skill/cron will be scoped to this agent specifically.\n` : '';
8201
- const builderPrefix = type === 'skill'
8202
- ? `[BUILDER MODE: You are helping build a reusable skill. ${agentContext}As you develop the procedure, output the current state as a JSON block:\n` +
8203
- '```json-artifact\n{"type":"skill","title":"...","description":"...","triggers":["..."],"steps":"markdown procedure","toolsUsed":["tool1","tool2"]}\n```\n' +
8204
- `Update this block in EVERY response as the skill evolves. If the user has linked tools, include them in the toolsUsed array. Ask clarifying questions to refine the procedure. Keep it conversational — one question at a time. ` +
8205
- `When the user says "save" or approves, output the final artifact block.]\n\n`
8206
- : type === 'cron'
8207
- ? `[BUILDER MODE: You are helping build a scheduled cron job. As you develop the job, output the current state as a JSON block:\n` +
8208
- '```json-artifact\n{"type":"cron","name":"...","schedule":"cron expression","tier":1,"prompt":"the full job prompt","mode":"standard","enabled":true}\n```\n' +
8209
- `Update this block in EVERY response as the job evolves. Ask about schedule, what it should do, which tools/APIs it needs, what tier (1=read-only, 2=read-write), and whether it should run in unleashed mode.\n` +
8210
- `When the user says "save" or approves, output the final artifact block.]\n\n`
8211
- : type === 'agent'
8212
- ? `[BUILDER MODE: You are helping create a new AI agent team member. As you develop the agent config, output the current state as a JSON block:\n` +
8213
- '```json-artifact\n{"type":"agent","name":"...","description":"role description","model":"sonnet","personality":"system prompt / onboarding brief","tools":["tool1","tool2"],"channel":"","tier":2}\n```\n' +
8214
- `Update this block in EVERY response as the agent evolves. Ask about: the agent's role, what tools it needs, what model to use, its personality/system prompt, which channel it should operate in, and its security tier.\n` +
8215
- `Keep it conversational — one question at a time. When the user says "save" or approves, output the final artifact block.]\n\n`
8216
- : type === 'workflow'
8217
- ? `[BUILDER MODE: You are helping the user DRAFT a "trick" — a (possibly multi-step) thing Clementine can do on a schedule or on demand. You are NOT executing the trick. You are not running anything in the background. You are only authoring a spec the user will save, then run later from the dashboard.\n` +
8218
- `\n` +
8219
- `Hard rules:\n` +
8220
- ` - NEVER say "on it", "running in the background", "I'll follow up", "working on it now", or anything else that implies you're executing the user's request. You are drafting a spec.\n` +
8221
- ` - Stay strictly conversational. One short question per turn. Update the artifact block on every turn.\n` +
8222
- ` - If the user describes "real work" (multi-step actions, scrapers, enrichments, reports), still just draft it — don't dispatch.\n` +
8223
- `\n` +
8224
- `As you develop the trick, output the current state as a JSON block:\n` +
8225
- '```json-artifact\n{"type":"workflow","name":"...","description":"...","schedule":"","model":"","steps":"step1:\\n prompt: ...\\nstep2:\\n prompt: ...\\n dependsOn: step1"}\n```\n' +
8226
- `Ask about (in roughly this order, one at a time):\n` +
8227
- ` 1. The goal (one sentence is fine — confirm it back).\n` +
8228
- ` 2. When it should run — natural language is fine ("every weekday at 9"); convert to a cron expression in the schedule field. Empty schedule = manual.\n` +
8229
- ` 3. Which tools, projects, or channels she'll need (MCP servers, local CLIs like sf/gh/gcloud, Slack/Discord targets).\n` +
8230
- ` 4. Which model — claude-opus-4-7 (most capable), claude-sonnet-4-6 (balanced), or claude-haiku-4-5-20251001 (fastest). Leave model empty if the user doesn't care.\n` +
8231
- `Most tricks need only one prompt step. Add steps only when the user explicitly wants a multi-step pipeline.\n` +
8232
- `When the user says "save" or approves, output the final artifact block — don't try to save it yourself, the dashboard handles persistence.]\n\n`
8233
- : `[BUILDER MODE: You are helping configure an artifact. Output structured JSON blocks as you build.]\n\n`;
8234
- enrichedMessage = builderPrefix + fileContext + toolContext + artifactContext + message;
8235
- builderSessionInited.add(sessionKey);
8236
- }
8237
- else {
8238
- enrichedMessage = fileContext + toolContext + artifactContext + message;
8239
- }
8098
+ const sessionKey = builderSessionKey(artifactType, agentSlug);
8240
8099
  try {
8241
8100
  writeEvent('progress', { status: 'thinking…' });
8242
8101
  const gateway = await getGateway();
8243
- // Builder generates JSON artifacts — no tool calls. Pin to 'none'
8244
- // toolset so the SDK system prompt drops the tool inventory.
8245
- gateway.setSessionToolset(sessionKey, 'none');
8102
+ const isFirstMessage = !gateway.assistant.getSdkSessionId(sessionKey);
8103
+ const enrichedMessage = buildBuilderEnrichedMessage({
8104
+ message,
8105
+ artifactType,
8106
+ agentSlug,
8107
+ currentArtifact,
8108
+ attachments,
8109
+ linkedTools,
8110
+ isFirstMessage,
8111
+ });
8246
8112
  let lastText = '';
8247
8113
  const response = await gateway.handleMessage(sessionKey, enrichedMessage, async (text) => {
8248
8114
  lastText = text ?? '';
@@ -8277,11 +8143,14 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
8277
8143
  }
8278
8144
  });
8279
8145
  // Reset builder session when user clicks "New"
8280
- app.post('/api/builder/reset', (_req, res) => {
8146
+ app.post('/api/builder/reset', async (_req, res) => {
8281
8147
  const { artifactType, agentSlug } = _req.body;
8282
- const type = artifactType || 'skill';
8283
- const sessionKey = `dashboard:builder:${type}:${agentSlug || 'clementine'}`;
8284
- builderSessionInited.delete(sessionKey);
8148
+ const sessionKey = builderSessionKey(artifactType, agentSlug);
8149
+ // Clearing the SDK session ID is what makes the next turn detect
8150
+ // as "first" and re-emit the system prefix. The per-process Set
8151
+ // has been replaced by the persisted SDK session map.
8152
+ const gateway = await getGateway();
8153
+ gateway.assistant.clearSession(sessionKey);
8285
8154
  res.json({ ok: true });
8286
8155
  });
8287
8156
  // Test a skill by sending a trigger message through the gateway with skill context
@@ -0,0 +1,36 @@
1
+ export type BuilderType = 'skill' | 'cron' | 'agent' | 'workflow';
2
+ export interface BuildBuilderMessageOptions {
3
+ /** What the user typed in the chat box. */
4
+ message: string;
5
+ /** Type of artifact being drafted. Defaults to 'skill'. */
6
+ artifactType?: string;
7
+ /** When set, the artifact is scoped to this hired agent. */
8
+ agentSlug?: string;
9
+ /** Current artifact JSON the dashboard is holding. Re-sent each
10
+ * turn so the agent has the live state in front of it. */
11
+ currentArtifact?: unknown;
12
+ /** Files the user dragged in for context. Each entry has a
13
+ * filename + base64-encoded content. Decoded + capped per file. */
14
+ attachments?: Array<{
15
+ filename?: string;
16
+ content?: string;
17
+ }>;
18
+ /** Pre-selected MCP / local tool names the artifact should use. */
19
+ linkedTools?: string[];
20
+ /** Whether this is the first turn of a new conversation (no prior
21
+ * SDK session). When true, the system prefix is included. */
22
+ isFirstMessage: boolean;
23
+ }
24
+ /**
25
+ * Compose the message that gets sent to gateway.handleMessage. The
26
+ * shape is: optional system prefix (first turn only), then file
27
+ * context, then linked-tools context, then current artifact state,
28
+ * then the user's literal message.
29
+ */
30
+ export declare function buildBuilderEnrichedMessage(opts: BuildBuilderMessageOptions): string;
31
+ /**
32
+ * Stable session-key for a builder conversation. Same key across
33
+ * /api/builder/chat and /api/builder/chat/stream so they share state.
34
+ */
35
+ export declare function builderSessionKey(artifactType: string | undefined, agentSlug: string | undefined): string;
36
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Build the enriched user message the trick builder sends to the
3
+ * canonical chat path. Single source of truth shared by both
4
+ * /api/builder/chat and /api/builder/chat/stream.
5
+ *
6
+ * Two non-obvious things:
7
+ *
8
+ * 1. "First message" detection uses the SDK session ID, not an
9
+ * in-memory Set. Daemon restart wipes in-memory state, so the
10
+ * prior implementation re-sent the system prefix on the very next
11
+ * turn even though SDK session resume kept the agent's memory of
12
+ * it. Anchoring detection to the persisted SDK session means
13
+ * the prefix is sent exactly once per genuine new conversation.
14
+ *
15
+ * 2. Model names referenced in the workflow prefix come from the
16
+ * MODELS config so they don't rot when models advance.
17
+ */
18
+ import { MODELS } from '../../config.js';
19
+ const FILE_MAX_CHARS = 4000;
20
+ function buildArtifactContext(currentArtifact) {
21
+ if (!currentArtifact)
22
+ return '';
23
+ return `\n[CURRENT ARTIFACT STATE]\n\`\`\`json-artifact\n${JSON.stringify(currentArtifact)}\n\`\`\`\n`;
24
+ }
25
+ function buildFileContext(attachments) {
26
+ if (!Array.isArray(attachments) || attachments.length === 0)
27
+ return '';
28
+ const parts = [];
29
+ for (const att of attachments) {
30
+ if (!att.filename || !att.content)
31
+ continue;
32
+ try {
33
+ const decoded = Buffer.from(att.content, 'base64').toString('utf-8');
34
+ const trimmed = decoded.length > FILE_MAX_CHARS
35
+ ? decoded.slice(0, FILE_MAX_CHARS) + '\n... (truncated)'
36
+ : decoded;
37
+ parts.push(`### ${att.filename}\n\`\`\`\n${trimmed}\n\`\`\``);
38
+ }
39
+ catch {
40
+ // Binary file; skip.
41
+ }
42
+ }
43
+ if (parts.length === 0)
44
+ return '';
45
+ return `\n[REFERENCE FILES — the user attached these for context]\n${parts.join('\n\n')}\n`;
46
+ }
47
+ function buildToolContext(linkedTools) {
48
+ if (!Array.isArray(linkedTools) || linkedTools.length === 0)
49
+ return '';
50
+ return `\n[LINKED TOOLS — this artifact should use these tools: ${linkedTools.join(', ')}]\n`;
51
+ }
52
+ function buildSystemPrefix(type, agentSlug) {
53
+ const agentContext = agentSlug
54
+ ? `You are building this for the agent "${agentSlug}". The artifact will be scoped to this agent specifically.\n`
55
+ : '';
56
+ if (type === 'skill') {
57
+ return `[BUILDER MODE: You are helping build a reusable skill. ${agentContext}As you develop the procedure, output the current state as a JSON block:\n` +
58
+ '```json-artifact\n{"type":"skill","title":"...","description":"...","triggers":["..."],"steps":"markdown procedure","toolsUsed":["tool1","tool2"]}\n```\n' +
59
+ `Update this block in EVERY response as the skill evolves. If the user has linked tools, include them in the toolsUsed array. Ask clarifying questions to refine the procedure. Keep it conversational — one question at a time. ` +
60
+ `When the user says "save" or approves, output the final artifact block.]\n\n`;
61
+ }
62
+ if (type === 'cron') {
63
+ return `[BUILDER MODE: You are helping build a scheduled cron job. ${agentContext}As you develop the job, output the current state as a JSON block:\n` +
64
+ '```json-artifact\n{"type":"cron","name":"...","schedule":"cron expression","tier":1,"prompt":"the full job prompt","enabled":true}\n```\n' +
65
+ `Update this block in EVERY response as the job evolves. Ask about schedule, what it should do, which tools/APIs it needs, and what tier (1=read-only, 2=read-write). ` +
66
+ `Cron jobs automatically pull in matching skills (learned procedures) at runtime. If the user describes a workflow that should be reusable, suggest creating it as a skill first, then building the cron job that references those trigger keywords.\n` +
67
+ `When the user says "save" or approves, output the final artifact block.]\n\n`;
68
+ }
69
+ if (type === 'agent') {
70
+ return `[BUILDER MODE: You are helping create a new AI agent team member. As you develop the agent config, output the current state as a JSON block:\n` +
71
+ '```json-artifact\n{"type":"agent","name":"...","description":"role description","model":"sonnet","personality":"system prompt / onboarding brief","tools":["tool1","tool2"],"channel":"","tier":2}\n```\n' +
72
+ `Update this block in EVERY response as the agent evolves. Ask about: the agent's role, what tools it needs, what model to use (haiku/sonnet/opus), its personality/system prompt, which channel it should operate in, and its security tier.\n` +
73
+ `Help the user think about what makes a good agent: clear role, specific tools, focused personality. Keep it conversational — one question at a time.\n` +
74
+ `When the user says "save" or approves, output the final artifact block.]\n\n`;
75
+ }
76
+ if (type === 'workflow') {
77
+ return `[BUILDER MODE: You are helping the user DRAFT a "trick" — a (possibly multi-step) thing Clementine can do on a schedule or on demand. You are NOT executing the trick. You are not running anything in the background. You are only authoring a spec the user will save, then run later from the dashboard.\n` +
78
+ `\n` +
79
+ `Hard rules:\n` +
80
+ ` - NEVER say "on it", "running in the background", "I'll follow up", "working on it now", or anything else that implies you're executing the user's request. You are drafting a spec.\n` +
81
+ ` - Stay strictly conversational. One short question per turn. Update the artifact block on every turn.\n` +
82
+ ` - If the user describes "real work" (multi-step actions, scrapers, enrichments, reports), still just draft it — don't dispatch.\n` +
83
+ `\n` +
84
+ `As you develop the trick, output the current state as a JSON block:\n` +
85
+ '```json-artifact\n{"type":"workflow","name":"...","description":"...","schedule":"","model":"","steps":"step1:\\n prompt: ...\\nstep2:\\n prompt: ...\\n dependsOn: step1"}\n```\n' +
86
+ `Ask about (in roughly this order, one at a time):\n` +
87
+ ` 1. The goal (one sentence is fine — confirm it back).\n` +
88
+ ` 2. When it should run — natural language is fine ("every weekday at 9"); convert to a cron expression in the schedule field. Empty schedule = manual.\n` +
89
+ ` 3. Which tools, projects, or channels she'll need (MCP servers, local CLIs like sf/gh/gcloud, Slack/Discord targets).\n` +
90
+ ` 4. Which model — ${MODELS.opus} (most capable), ${MODELS.sonnet} (balanced), or ${MODELS.haiku} (fastest). Leave model empty if the user doesn't care.\n` +
91
+ `Most tricks need only one prompt step. Add steps only when the user explicitly wants a multi-step pipeline.\n` +
92
+ `When the user says "save" or approves, output the final artifact block — don't try to save it yourself, the dashboard handles persistence.]\n\n`;
93
+ }
94
+ return `[BUILDER MODE: You are helping configure an artifact. Output structured JSON blocks as you build.]\n\n`;
95
+ }
96
+ /**
97
+ * Compose the message that gets sent to gateway.handleMessage. The
98
+ * shape is: optional system prefix (first turn only), then file
99
+ * context, then linked-tools context, then current artifact state,
100
+ * then the user's literal message.
101
+ */
102
+ export function buildBuilderEnrichedMessage(opts) {
103
+ const type = opts.artifactType || 'skill';
104
+ const fileContext = buildFileContext(opts.attachments);
105
+ const toolContext = buildToolContext(opts.linkedTools);
106
+ const artifactContext = buildArtifactContext(opts.currentArtifact);
107
+ const prefix = opts.isFirstMessage ? buildSystemPrefix(type, opts.agentSlug) : '';
108
+ return prefix + fileContext + toolContext + artifactContext + opts.message;
109
+ }
110
+ /**
111
+ * Stable session-key for a builder conversation. Same key across
112
+ * /api/builder/chat and /api/builder/chat/stream so they share state.
113
+ */
114
+ export function builderSessionKey(artifactType, agentSlug) {
115
+ const type = artifactType || 'skill';
116
+ return `dashboard:builder:${type}:${agentSlug || 'clementine'}`;
117
+ }
118
+ //# sourceMappingURL=prompt.js.map
@@ -1780,53 +1780,58 @@ export class Gateway {
1780
1780
  const { runAgent } = await import('../agent/run-agent.js');
1781
1781
  const { buildExtraMcpForRunAgent } = await import('../agent/run-agent-mcp.js');
1782
1782
  const { buildChatSystemAppend } = await import('../agent/run-agent-context.js');
1783
- // Wire Composio + external MCP servers (Outlook, Gmail,
1784
- // Salesforce, etc) so chat can reach the same tools the
1785
- // legacy chat path did. Profile allowlists override the
1786
- // bundle router when set.
1787
- //
1788
- // Use originalText (not chatPrompt) for scope routing
1789
- // chatPrompt may have the partial-interrupt banner folded in,
1790
- // which would skew bundle matching.
1791
- const chatMcp = await buildExtraMcpForRunAgent({
1792
- scopeText: originalText,
1793
- profile: resolvedProfile,
1794
- });
1795
- // Inject vault context (SOUL.md / MEMORY.md / AGENTS.md +
1796
- // optional profile body) into the system-prompt append so
1797
- // the agent has personality + long-term memory + team
1798
- // awareness. Profile-specific MEMORY.md takes precedence
1799
- // over the global one when a hired agent is active.
1800
- const chatSystemAppend = buildChatSystemAppend({
1801
- profile: resolvedProfile,
1802
- profileAppend: resolvedProfile?.systemPromptBody,
1803
- });
1804
- // Per-turn context — recall of recent transcripts, persistent
1805
- // learnings, silent context blocks, security advisories, and
1806
- // toolset directives accumulated above. This is what gives
1807
- // continuity across daemon restarts (SDK in-memory session is
1808
- // gone, but transcripts in SQLite persist; recall surfaces
1809
- // them). Prefixed to the user message in a clearly-delimited
1810
- // [Context] block so the model knows it's framing, not user
1811
- // input.
1812
- const turnContextPrefix = securityAnnotation.trim()
1783
+ // Builder sessions (dashboard trick/skill/cron/agent builder)
1784
+ // are conversational JSON-drafting flows, not real chat. They
1785
+ // don't need vault context, MCP tools, recall, or auto-memory
1786
+ // extraction the builder prefix IS the system prompt and
1787
+ // the agent only emits json-artifact blocks. Strip everything
1788
+ // expensive; keep just SDK session resume so multi-turn
1789
+ // artifact iteration sees its own prior turns.
1790
+ const isBuilderSession = sessionKey.startsWith('dashboard:builder:');
1791
+ // Wire Composio + external MCP only for real chat. Builder
1792
+ // skips entirely — builder turns never call tools.
1793
+ const chatMcp = isBuilderSession
1794
+ ? null
1795
+ : await buildExtraMcpForRunAgent({
1796
+ scopeText: originalText,
1797
+ profile: resolvedProfile,
1798
+ });
1799
+ // Vault context (SOUL.md / MEMORY.md / AGENTS.md + optional
1800
+ // profile body) — real chat only. Builder gets just its own
1801
+ // prefix as the system prompt.
1802
+ const chatSystemAppend = isBuilderSession
1803
+ ? ''
1804
+ : buildChatSystemAppend({
1805
+ profile: resolvedProfile,
1806
+ profileAppend: resolvedProfile?.systemPromptBody,
1807
+ });
1808
+ // Per-turn context (recall + persistent learnings + silent
1809
+ // blocks + security/toolset directives) real chat only.
1810
+ // Builder doesn't need recall of unrelated transcripts.
1811
+ const turnContextPrefix = !isBuilderSession && securityAnnotation.trim()
1813
1812
  ? `[Context — read this for continuity, then respond to the user message below]\n${securityAnnotation}\n[/Context]\n\n`
1814
1813
  : '';
1815
1814
  const finalPrompt = turnContextPrefix + chatPrompt;
1816
1815
  // Resume the prior SDK session when one exists for this
1817
1816
  // sessionKey. The SDK persists session JSONLs to disk, so
1818
- // resume works across daemon restarts. Without this, every
1819
- // turn is a fresh SDK session with zero conversation history.
1817
+ // resume works across daemon restarts AND for builder
1818
+ // multi-turn artifact iteration.
1820
1819
  const priorSdkSessionId = this.assistant.getSdkSessionId(effectiveSessionKey);
1820
+ // Builder cost knobs: Haiku is plenty for JSON drafting,
1821
+ // tight budget, no tools surfaced in the system prompt.
1822
+ const builderModel = isBuilderSession ? MODELS.haiku : effectiveModel;
1823
+ const builderBudget = isBuilderSession ? 0.10 : undefined;
1824
+ const builderAllowedTools = isBuilderSession ? [] : undefined;
1821
1825
  logger.info({
1822
1826
  sessionKey: effectiveSessionKey,
1823
1827
  profile: resolvedProfile?.slug,
1824
- path: 'runagent_chat',
1825
- composioConnected: chatMcp.composioConnected.length,
1826
- externalConnected: chatMcp.externalConnected.length,
1828
+ path: isBuilderSession ? 'runagent_builder' : 'runagent_chat',
1829
+ composioConnected: chatMcp?.composioConnected.length ?? 0,
1830
+ externalConnected: chatMcp?.externalConnected.length ?? 0,
1827
1831
  systemAppendChars: chatSystemAppend.length,
1828
1832
  turnContextChars: turnContextPrefix.length,
1829
1833
  resumingSdkSessionId: priorSdkSessionId || null,
1834
+ isBuilderSession,
1830
1835
  }, 'Routing chat through runAgent');
1831
1836
  const runAgentResult = await runAgent(finalPrompt, {
1832
1837
  sessionKey: effectiveSessionKey,
@@ -1834,11 +1839,13 @@ export class Gateway {
1834
1839
  profile: resolvedProfile,
1835
1840
  agentManager: this.getAgentManager(),
1836
1841
  memoryStore: this.assistant.getMemoryStore?.() ?? null,
1837
- ...(effectiveModel ? { model: effectiveModel } : {}),
1842
+ ...(builderModel ? { model: builderModel } : {}),
1838
1843
  ...(maxTurns ? { maxTurns } : {}),
1844
+ ...(builderBudget !== undefined ? { maxBudgetUsd: builderBudget } : {}),
1845
+ ...(builderAllowedTools ? { allowedTools: builderAllowedTools } : {}),
1839
1846
  ...(chatSystemAppend ? { systemPromptAppend: chatSystemAppend } : {}),
1840
1847
  ...(priorSdkSessionId ? { resumeSessionId: priorSdkSessionId } : {}),
1841
- extraMcpServers: chatMcp.servers,
1848
+ ...(chatMcp ? { extraMcpServers: chatMcp.servers } : {}),
1842
1849
  onText: wrappedOnText,
1843
1850
  onToolActivity: ({ tool, input }) => {
1844
1851
  toolActivityCount++;
@@ -1856,9 +1863,11 @@ export class Gateway {
1856
1863
  }
1857
1864
  clearTimeout(chatTimer);
1858
1865
  clearTimeout(hardWallTimer);
1859
- // Mirror transcript so memory + recall continue working.
1866
+ // Mirror transcript so memory + recall continue working — but
1867
+ // skip for builder sessions since their turns are spec-drafting,
1868
+ // not real conversation worth recalling later.
1860
1869
  const memoryStore = this.assistant.getMemoryStore?.();
1861
- if (memoryStore) {
1870
+ if (memoryStore && !isBuilderSession) {
1862
1871
  try {
1863
1872
  memoryStore.saveTurn(effectiveSessionKey, 'user', originalText);
1864
1873
  memoryStore.saveTurn(effectiveSessionKey, 'assistant', runAgentResult.text);
@@ -1867,10 +1876,13 @@ export class Gateway {
1867
1876
  logger.debug({ err }, 'chat: transcript mirror failed (non-fatal)');
1868
1877
  }
1869
1878
  }
1870
- // Fire auto-memory extraction in the background.
1871
- this.assistant
1872
- .triggerMemoryExtractionPostExchange(originalText, runAgentResult.text, effectiveSessionKey, resolvedProfile)
1873
- .catch(err => logger.debug({ err, sessionKey: effectiveSessionKey }, 'chat: auto-memory failed (non-fatal)'));
1879
+ // Fire auto-memory extraction in the background — builder
1880
+ // turns are JSON-drafting noise, not memorable exchanges.
1881
+ if (!isBuilderSession) {
1882
+ this.assistant
1883
+ .triggerMemoryExtractionPostExchange(originalText, runAgentResult.text, effectiveSessionKey, resolvedProfile)
1884
+ .catch(err => logger.debug({ err, sessionKey: effectiveSessionKey }, 'chat: auto-memory failed (non-fatal)'));
1885
+ }
1874
1886
  // Auth recovered if we got a clean response.
1875
1887
  this.clearAuthFailure();
1876
1888
  logger.info({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.54",
3
+ "version": "1.18.56",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",