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.
- package/dist/cli/dashboard.js +34 -165
- package/dist/dashboard/builder/prompt.d.ts +36 -0
- package/dist/dashboard/builder/prompt.js +118 -0
- package/dist/gateway/router.js +55 -43
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
8111
|
-
//
|
|
8112
|
-
//
|
|
8113
|
-
//
|
|
8114
|
-
|
|
8115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8244
|
-
|
|
8245
|
-
|
|
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
|
|
8283
|
-
|
|
8284
|
-
|
|
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
|
package/dist/gateway/router.js
CHANGED
|
@@ -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
|
-
//
|
|
1784
|
-
//
|
|
1785
|
-
//
|
|
1786
|
-
//
|
|
1787
|
-
//
|
|
1788
|
-
//
|
|
1789
|
-
//
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
//
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
//
|
|
1809
|
-
//
|
|
1810
|
-
//
|
|
1811
|
-
|
|
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
|
|
1819
|
-
// turn
|
|
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
|
|
1826
|
-
externalConnected: chatMcp
|
|
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
|
-
...(
|
|
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
|
-
|
|
1872
|
-
|
|
1873
|
-
.
|
|
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({
|