clementine-agent 1.18.170 → 1.18.171
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/background-tasks.js +13 -0
- package/dist/agent/complex-task-detector.d.ts +9 -0
- package/dist/agent/complex-task-detector.js +118 -0
- package/dist/cli/dashboard.js +199 -23
- package/dist/gateway/router.d.ts +21 -1
- package/dist/gateway/router.js +230 -10
- package/dist/gateway/turn-ledger.d.ts +7 -0
- package/dist/gateway/turn-ledger.js +7 -1
- package/dist/tools/background-task-tools.js +4 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -113,6 +113,14 @@ export function markRunning(id, opts) {
|
|
|
113
113
|
safeWrite(pathFor(id, opts), task);
|
|
114
114
|
return task;
|
|
115
115
|
}
|
|
116
|
+
function writeFullResultFile(id, result, opts) {
|
|
117
|
+
if (result.length <= RESULT_TRUNCATE_BYTES)
|
|
118
|
+
return undefined;
|
|
119
|
+
const file = path.join(dirFor(opts), `${id}.result.md`);
|
|
120
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
121
|
+
writeFileSync(file, result);
|
|
122
|
+
return file;
|
|
123
|
+
}
|
|
116
124
|
/** Transition to 'done' with final result. */
|
|
117
125
|
export function markDone(id, result, deliverableNote, opts) {
|
|
118
126
|
const task = loadBackgroundTask(id, opts);
|
|
@@ -122,9 +130,14 @@ export function markDone(id, result, deliverableNote, opts) {
|
|
|
122
130
|
return task;
|
|
123
131
|
task.status = 'done';
|
|
124
132
|
task.completedAt = new Date().toISOString();
|
|
133
|
+
const resultPath = writeFullResultFile(id, result, opts);
|
|
134
|
+
if (resultPath)
|
|
135
|
+
task.resultPath = resultPath;
|
|
125
136
|
task.result = result;
|
|
126
137
|
if (deliverableNote)
|
|
127
138
|
task.deliverableNote = deliverableNote;
|
|
139
|
+
else if (resultPath)
|
|
140
|
+
task.deliverableNote = resultPath;
|
|
128
141
|
safeWrite(pathFor(id, opts), task);
|
|
129
142
|
return task;
|
|
130
143
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ComplexTaskRecommendation {
|
|
2
|
+
score: number;
|
|
3
|
+
reasons: string[];
|
|
4
|
+
suggestedMaxMinutes: number;
|
|
5
|
+
plan: string[];
|
|
6
|
+
queueImmediately: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function detectComplexTaskForBackground(text: string): ComplexTaskRecommendation | null;
|
|
9
|
+
//# sourceMappingURL=complex-task-detector.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const SKILL_AUTHORING_RE = /\b(create|make|build|draft|write|teach|save|update)\b.{0,40}\b(skill|SKILL\.md)\b|\bskill[- ]creator\b/i;
|
|
2
|
+
const EXPLICIT_BACKGROUND_RE = /\b(background|deep mode|keep working|don't stop|dont stop|autonomous|long[- ]running|run overnight|take your time)\b/i;
|
|
3
|
+
const COMPLEX_WORK_RE = /\b(audit|research|analy[sz]e|review|scrape|crawl|extract|enrich|compile|compare|verify|cross[- ]check|triage|reconcile|draft|generate|update|sync|report back|write back)\b/i;
|
|
4
|
+
const BATCH_RE = /\b(all|every|each|bulk|batch|list of|contacts?|leads?|accounts?|tasks?|tickets?|records?|rows?|pages?|repos?|projects?)\b/i;
|
|
5
|
+
const SIDE_EFFECT_RE = /\b(update|write|create|draft|send|post|comment|reply|upload|append|sync|mark|close|move)\b/i;
|
|
6
|
+
const MULTI_STEP_RE = /\b(and then|then|after that|finally|from .* to |against .* and |across|compile .* into|check .* then)\b/i;
|
|
7
|
+
const SYSTEM_KEYWORDS = [
|
|
8
|
+
'asana',
|
|
9
|
+
'salesforce',
|
|
10
|
+
'google sheet',
|
|
11
|
+
'google sheets',
|
|
12
|
+
'sheet',
|
|
13
|
+
'sheets',
|
|
14
|
+
'dataforseo',
|
|
15
|
+
'hubspot',
|
|
16
|
+
'notion',
|
|
17
|
+
'github',
|
|
18
|
+
'gmail',
|
|
19
|
+
'outlook',
|
|
20
|
+
'slack',
|
|
21
|
+
'discord',
|
|
22
|
+
'website',
|
|
23
|
+
'websites',
|
|
24
|
+
'crm',
|
|
25
|
+
'spreadsheet',
|
|
26
|
+
'csv',
|
|
27
|
+
'airtable',
|
|
28
|
+
'linear',
|
|
29
|
+
'jira',
|
|
30
|
+
];
|
|
31
|
+
function countSystemMentions(text) {
|
|
32
|
+
const lower = text.toLowerCase();
|
|
33
|
+
let count = 0;
|
|
34
|
+
for (const keyword of SYSTEM_KEYWORDS) {
|
|
35
|
+
if (lower.includes(keyword))
|
|
36
|
+
count++;
|
|
37
|
+
}
|
|
38
|
+
return count;
|
|
39
|
+
}
|
|
40
|
+
function estimatedMinutes(score, systemCount) {
|
|
41
|
+
if (score >= 8 || systemCount >= 4)
|
|
42
|
+
return 90;
|
|
43
|
+
if (score >= 6 || systemCount >= 3)
|
|
44
|
+
return 60;
|
|
45
|
+
return 30;
|
|
46
|
+
}
|
|
47
|
+
function buildPlan(text, systemCount) {
|
|
48
|
+
const lower = text.toLowerCase();
|
|
49
|
+
const plan = [];
|
|
50
|
+
plan.push('Confirm the exact scope, filters, and write/send permissions before making side-effecting changes.');
|
|
51
|
+
if (systemCount > 0) {
|
|
52
|
+
plan.push('Connect to the named systems with official MCP/API/CLI tools and use the narrowest reliable query.');
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
plan.push('Gather the source material with the available project, file, web, memory, or CLI tools.');
|
|
56
|
+
}
|
|
57
|
+
if (BATCH_RE.test(text)) {
|
|
58
|
+
plan.push('Process records in batches, track counts, and keep skipped/error records separate.');
|
|
59
|
+
}
|
|
60
|
+
if (lower.includes('enrich') || lower.includes('dataforseo')) {
|
|
61
|
+
plan.push('Enrich only qualified records and keep the signal used for each output row or draft.');
|
|
62
|
+
}
|
|
63
|
+
if (SIDE_EFFECT_RE.test(text)) {
|
|
64
|
+
plan.push('Create drafts or updates first; only send or commit irreversible changes after explicit approval.');
|
|
65
|
+
}
|
|
66
|
+
plan.push('Return a concise final report with counts, changed locations, failures, and recommended next action.');
|
|
67
|
+
return plan.slice(0, 6);
|
|
68
|
+
}
|
|
69
|
+
export function detectComplexTaskForBackground(text) {
|
|
70
|
+
const trimmed = text.trim();
|
|
71
|
+
if (!trimmed)
|
|
72
|
+
return null;
|
|
73
|
+
if (SKILL_AUTHORING_RE.test(trimmed))
|
|
74
|
+
return null;
|
|
75
|
+
const systemCount = countSystemMentions(trimmed);
|
|
76
|
+
const reasons = [];
|
|
77
|
+
let score = 0;
|
|
78
|
+
if (EXPLICIT_BACKGROUND_RE.test(trimmed)) {
|
|
79
|
+
score += 4;
|
|
80
|
+
reasons.push('explicit background/deep-work wording');
|
|
81
|
+
}
|
|
82
|
+
if (COMPLEX_WORK_RE.test(trimmed)) {
|
|
83
|
+
score += 2;
|
|
84
|
+
reasons.push('multi-step work verb');
|
|
85
|
+
}
|
|
86
|
+
if (BATCH_RE.test(trimmed)) {
|
|
87
|
+
score += 2;
|
|
88
|
+
reasons.push('batch or many-record scope');
|
|
89
|
+
}
|
|
90
|
+
if (SIDE_EFFECT_RE.test(trimmed)) {
|
|
91
|
+
score += 1;
|
|
92
|
+
reasons.push('write/draft/update side effects');
|
|
93
|
+
}
|
|
94
|
+
if (MULTI_STEP_RE.test(trimmed)) {
|
|
95
|
+
score += 1;
|
|
96
|
+
reasons.push('multi-step sequencing');
|
|
97
|
+
}
|
|
98
|
+
if (systemCount >= 2) {
|
|
99
|
+
score += Math.min(4, systemCount);
|
|
100
|
+
reasons.push(`${systemCount} named systems or data surfaces`);
|
|
101
|
+
}
|
|
102
|
+
if (trimmed.length > 450) {
|
|
103
|
+
score += 1;
|
|
104
|
+
reasons.push('long detailed request');
|
|
105
|
+
}
|
|
106
|
+
const queueImmediately = EXPLICIT_BACKGROUND_RE.test(trimmed) && score >= 5;
|
|
107
|
+
const shouldOffer = queueImmediately || score >= 5 || (systemCount >= 2 && (BATCH_RE.test(trimmed) || SIDE_EFFECT_RE.test(trimmed)));
|
|
108
|
+
if (!shouldOffer)
|
|
109
|
+
return null;
|
|
110
|
+
return {
|
|
111
|
+
score,
|
|
112
|
+
reasons,
|
|
113
|
+
suggestedMaxMinutes: estimatedMinutes(score, systemCount),
|
|
114
|
+
plan: buildPlan(trimmed, systemCount),
|
|
115
|
+
queueImmediately,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=complex-task-detector.js.map
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -2936,6 +2936,40 @@ export async function cmdDashboard(opts) {
|
|
|
2936
2936
|
res.status(500).json({ error: String(err).slice(0, 200) });
|
|
2937
2937
|
}
|
|
2938
2938
|
});
|
|
2939
|
+
app.post('/api/background-offers/:id/accept', async (req, res) => {
|
|
2940
|
+
try {
|
|
2941
|
+
const id = req.params.id;
|
|
2942
|
+
if (!/^bo-[a-z0-9]+-[a-z0-9]{3,10}$/.test(id)) {
|
|
2943
|
+
res.status(400).json({ error: 'Invalid background offer id' });
|
|
2944
|
+
return;
|
|
2945
|
+
}
|
|
2946
|
+
const gateway = await getGateway();
|
|
2947
|
+
const result = gateway.acceptBackgroundOffer('dashboard:web', id);
|
|
2948
|
+
if (!result.ok) {
|
|
2949
|
+
res.status(404).json({ ok: false, error: result.response });
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
res.json(result);
|
|
2953
|
+
}
|
|
2954
|
+
catch (err) {
|
|
2955
|
+
res.status(500).json({ error: String(err).slice(0, 200) });
|
|
2956
|
+
}
|
|
2957
|
+
});
|
|
2958
|
+
app.post('/api/background-offers/:id/dismiss', async (req, res) => {
|
|
2959
|
+
try {
|
|
2960
|
+
const id = req.params.id;
|
|
2961
|
+
if (!/^bo-[a-z0-9]+-[a-z0-9]{3,10}$/.test(id)) {
|
|
2962
|
+
res.status(400).json({ error: 'Invalid background offer id' });
|
|
2963
|
+
return;
|
|
2964
|
+
}
|
|
2965
|
+
const gateway = await getGateway();
|
|
2966
|
+
const result = gateway.dismissBackgroundOffer('dashboard:web', id);
|
|
2967
|
+
res.status(result.ok ? 200 : 404).json(result.ok ? result : { ok: false, error: result.response });
|
|
2968
|
+
}
|
|
2969
|
+
catch (err) {
|
|
2970
|
+
res.status(500).json({ error: String(err).slice(0, 200) });
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2939
2973
|
app.get('/api/heartbeat/agent/:slug', (req, res) => {
|
|
2940
2974
|
const slug = req.params.slug;
|
|
2941
2975
|
const state = getHeartbeat();
|
|
@@ -21628,9 +21662,13 @@ if('serviceWorker' in navigator){navigator.serviceWorker.getRegistrations().then
|
|
|
21628
21662
|
<button type="button" class="skill-composer-mode" data-kind="memory" onclick="setSkillComposerMode('memory')" style="padding:7px 6px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-secondary);font-size:11px;font-weight:600;cursor:pointer">Memory</button>
|
|
21629
21663
|
</div>
|
|
21630
21664
|
<label id="skill-composer-anchor-label" for="skill-composer-anchor" style="display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;margin-bottom:6px">Anchor</label>
|
|
21631
|
-
<
|
|
21665
|
+
<div style="display:flex;gap:6px;align-items:center">
|
|
21666
|
+
<input id="skill-composer-anchor" list="skill-composer-anchor-options" oninput="updateSkillComposerDraftState()" onkeydown="if(event.key==='Enter'){event.preventDefault();addSkillComposerDependency();}" placeholder="Optional tool, project, command, or memory source" style="flex:1;min-width:0;box-sizing:border-box;padding:9px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);color:var(--text-primary);font-size:12px">
|
|
21667
|
+
<button type="button" class="btn-secondary" onclick="addSkillComposerDependency()" style="font-size:12px;padding:8px 10px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-primary);cursor:pointer">Add</button>
|
|
21668
|
+
</div>
|
|
21632
21669
|
<datalist id="skill-composer-anchor-options"></datalist>
|
|
21633
21670
|
<div id="skill-composer-anchor-summary" style="margin-top:8px;min-height:22px;font-size:11px;color:var(--text-muted);line-height:1.45">No starting point selected.</div>
|
|
21671
|
+
<div id="skill-composer-preview" style="margin-top:10px;max-height:180px;overflow:auto;border:1px solid var(--border);border-radius:6px;background:var(--bg-primary);padding:10px 12px;font-size:11px;line-height:1.45;color:var(--text-secondary)"></div>
|
|
21634
21672
|
<div style="display:flex;align-items:center;justify-content:flex-end;gap:8px;margin-top:12px">
|
|
21635
21673
|
<button type="button" class="btn-secondary" onclick="openSkillStudio()" style="font-size:12px;padding:7px 12px;border-radius:6px;border:1px solid var(--border);background:transparent;color:var(--text-primary);cursor:pointer">Open Studio</button>
|
|
21636
21674
|
<button type="button" class="btn-primary" id="skill-composer-draft-btn" onclick="startSkillComposerDraft()" disabled style="font-size:12px;padding:7px 14px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-weight:600;cursor:pointer;opacity:0.55">Draft skill</button>
|
|
@@ -28725,6 +28763,7 @@ var _skillsState = {
|
|
|
28725
28763
|
|
|
28726
28764
|
var _skillComposerMode = 'outcome';
|
|
28727
28765
|
var _skillComposerOptionsCache = null;
|
|
28766
|
+
var _skillComposerDependencies = [];
|
|
28728
28767
|
|
|
28729
28768
|
var _skillComposerCopy = {
|
|
28730
28769
|
outcome: {
|
|
@@ -28785,34 +28824,77 @@ function updateSkillComposerDraftState() {
|
|
|
28785
28824
|
var text = (document.getElementById('skill-composer-text') || {}).value || '';
|
|
28786
28825
|
var anchor = (document.getElementById('skill-composer-anchor') || {}).value || '';
|
|
28787
28826
|
var btn = document.getElementById('skill-composer-draft-btn');
|
|
28788
|
-
var enabled = !!(text.trim() || anchor.trim());
|
|
28827
|
+
var enabled = !!(text.trim() || anchor.trim() || _skillComposerDependencies.length);
|
|
28789
28828
|
if (btn) {
|
|
28790
28829
|
btn.disabled = !enabled;
|
|
28791
28830
|
btn.style.opacity = enabled ? '1' : '0.55';
|
|
28792
28831
|
btn.style.cursor = enabled ? 'pointer' : 'not-allowed';
|
|
28793
28832
|
}
|
|
28794
28833
|
renderSkillComposerAnchorSummary(text, anchor);
|
|
28834
|
+
renderSkillComposerPreview();
|
|
28795
28835
|
}
|
|
28796
28836
|
|
|
28797
28837
|
function renderSkillComposerAnchorSummary(text, anchor) {
|
|
28798
28838
|
var box = document.getElementById('skill-composer-anchor-summary');
|
|
28799
28839
|
if (!box) return;
|
|
28800
28840
|
var trimmed = String(anchor || '').trim();
|
|
28801
|
-
|
|
28841
|
+
var deps = getSkillComposerDependencies(false);
|
|
28842
|
+
if (!trimmed && deps.length === 0) {
|
|
28802
28843
|
box.innerHTML = '<span style="color:var(--text-muted)">No starting point selected.</span>';
|
|
28803
28844
|
return;
|
|
28804
28845
|
}
|
|
28805
|
-
var
|
|
28806
|
-
var
|
|
28807
|
-
|
|
28808
|
-
|
|
28809
|
-
|
|
28810
|
-
|
|
28811
|
-
|
|
28812
|
-
|
|
28813
|
-
|
|
28814
|
-
|
|
28815
|
-
|
|
28846
|
+
var html = '<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">';
|
|
28847
|
+
for (var i = 0; i < deps.length; i++) {
|
|
28848
|
+
var dep = deps[i];
|
|
28849
|
+
var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
|
|
28850
|
+
html += '<span style="display:inline-flex;align-items:center;gap:5px;max-width:100%;padding:2px 7px;border:1px solid var(--border);border-radius:999px;background:var(--bg-primary);color:var(--text-primary);font-size:10px">'
|
|
28851
|
+
+ '<strong style="color:var(--accent);font-weight:600">' + esc(copy.promptLabel) + '</strong>'
|
|
28852
|
+
+ '<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:190px">' + esc(dep.value) + '</span>'
|
|
28853
|
+
+ '<button type="button" onclick="removeSkillComposerDependency(' + i + ')" title="Remove" style="border:none;background:transparent;color:var(--text-muted);cursor:pointer;padding:0;font-size:12px;line-height:1">×</button>'
|
|
28854
|
+
+ '</span>';
|
|
28855
|
+
}
|
|
28856
|
+
if (trimmed) {
|
|
28857
|
+
var modeCopy = _skillComposerCopy[_skillComposerMode] || _skillComposerCopy.outcome;
|
|
28858
|
+
var tools = inferSkillComposerTools(_skillComposerMode, trimmed);
|
|
28859
|
+
html += '<span style="color:var(--text-muted)">Pending ' + esc(modeCopy.promptLabel) + ':</span>'
|
|
28860
|
+
+ '<code style="font-size:10px;background:var(--bg-primary);border:1px solid var(--border);padding:1px 5px;border-radius:4px;color:var(--text-primary);max-width:100%;overflow:hidden;text-overflow:ellipsis">' + esc(trimmed) + '</code>'
|
|
28861
|
+
+ (tools.length ? '<span style="color:var(--text-muted)">adds ' + esc(tools.join(', ')) + '</span>' : '');
|
|
28862
|
+
}
|
|
28863
|
+
html += '</div>';
|
|
28864
|
+
box.innerHTML = html;
|
|
28865
|
+
}
|
|
28866
|
+
|
|
28867
|
+
function getSkillComposerDependencies(includePending) {
|
|
28868
|
+
var deps = _skillComposerDependencies.slice();
|
|
28869
|
+
if (includePending) {
|
|
28870
|
+
var pending = ((document.getElementById('skill-composer-anchor') || {}).value || '').trim();
|
|
28871
|
+
if (pending) deps.push({ mode: _skillComposerMode || 'outcome', value: pending });
|
|
28872
|
+
}
|
|
28873
|
+
var seen = {};
|
|
28874
|
+
return deps.filter(function(dep) {
|
|
28875
|
+
var key = dep.mode + ':' + dep.value;
|
|
28876
|
+
if (seen[key]) return false;
|
|
28877
|
+
seen[key] = true;
|
|
28878
|
+
return true;
|
|
28879
|
+
});
|
|
28880
|
+
}
|
|
28881
|
+
|
|
28882
|
+
function addSkillComposerDependency() {
|
|
28883
|
+
var input = document.getElementById('skill-composer-anchor');
|
|
28884
|
+
var value = (input && input.value || '').trim();
|
|
28885
|
+
if (!value) return;
|
|
28886
|
+
var mode = _skillComposerMode || 'outcome';
|
|
28887
|
+
var key = mode + ':' + value;
|
|
28888
|
+
var exists = _skillComposerDependencies.some(function(dep) { return dep.mode + ':' + dep.value === key; });
|
|
28889
|
+
if (!exists) _skillComposerDependencies.push({ mode: mode, value: value });
|
|
28890
|
+
if (input) input.value = '';
|
|
28891
|
+
updateSkillComposerDraftState();
|
|
28892
|
+
}
|
|
28893
|
+
|
|
28894
|
+
function removeSkillComposerDependency(index) {
|
|
28895
|
+
if (index < 0 || index >= _skillComposerDependencies.length) return;
|
|
28896
|
+
_skillComposerDependencies.splice(index, 1);
|
|
28897
|
+
updateSkillComposerDraftState();
|
|
28816
28898
|
}
|
|
28817
28899
|
|
|
28818
28900
|
function inferSkillComposerTools(mode, anchor) {
|
|
@@ -28888,12 +28970,19 @@ function slugifySkillTitle(title) {
|
|
|
28888
28970
|
|
|
28889
28971
|
function buildSkillComposerDraftSeed() {
|
|
28890
28972
|
var text = ((document.getElementById('skill-composer-text') || {}).value || '').trim();
|
|
28891
|
-
var anchor = ((document.getElementById('skill-composer-anchor') || {}).value || '').trim();
|
|
28892
28973
|
var mode = _skillComposerMode || 'outcome';
|
|
28893
28974
|
var modeCopy = _skillComposerCopy[mode] || _skillComposerCopy.outcome;
|
|
28975
|
+
var deps = getSkillComposerDependencies(true);
|
|
28976
|
+
var anchor = deps.map(function(dep) {
|
|
28977
|
+
var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
|
|
28978
|
+
return copy.promptLabel + ': ' + dep.value;
|
|
28979
|
+
}).join('; ');
|
|
28894
28980
|
var title = inferSkillComposerTitle(text, anchor);
|
|
28895
28981
|
var name = slugifySkillTitle(title);
|
|
28896
|
-
var tools =
|
|
28982
|
+
var tools = [];
|
|
28983
|
+
for (var di = 0; di < deps.length; di++) {
|
|
28984
|
+
tools = tools.concat(inferSkillComposerTools(deps[di].mode, deps[di].value));
|
|
28985
|
+
}
|
|
28897
28986
|
var hay = (text + ' ' + anchor).toLowerCase();
|
|
28898
28987
|
if (hay.indexOf('salesforce') !== -1) tools = dedupeSkillComposerToolNames(tools.concat(['Bash']));
|
|
28899
28988
|
if (hay.indexOf('dataforseo') !== -1 && tools.indexOf('mcp__dataforseo__*') === -1) tools.push('mcp__dataforseo__*');
|
|
@@ -28909,13 +28998,18 @@ function buildSkillComposerDraftSeed() {
|
|
|
28909
28998
|
if (description.length > 1024) description = description.slice(0, 1024);
|
|
28910
28999
|
|
|
28911
29000
|
var toolLines = tools.length ? tools.map(function(t) { return '- ' + t; }).join('\\n') : '- No explicit allowlist yet; add only the tools this skill truly needs.';
|
|
28912
|
-
var anchorLine =
|
|
29001
|
+
var anchorLine = deps.length
|
|
29002
|
+
? deps.map(function(dep) {
|
|
29003
|
+
var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
|
|
29004
|
+
return '- ' + copy.promptLabel + ': ' + dep.value;
|
|
29005
|
+
}).join('\\n')
|
|
29006
|
+
: '- None selected';
|
|
28913
29007
|
var body = '# ' + title + '\\n\\n'
|
|
28914
29008
|
+ '## Use When\\n\\n'
|
|
28915
29009
|
+ description + '\\n\\n'
|
|
28916
29010
|
+ '## Starting Point\\n\\n'
|
|
28917
29011
|
+ '- Outcome: ' + (text || 'Fill in the exact user goal before saving.') + '\\n'
|
|
28918
|
-
+
|
|
29012
|
+
+ anchorLine + '\\n\\n'
|
|
28919
29013
|
+ '## Required Tools\\n\\n'
|
|
28920
29014
|
+ toolLines + '\\n\\n'
|
|
28921
29015
|
+ '## Procedure\\n\\n'
|
|
@@ -28961,10 +29055,37 @@ function buildSkillComposerDraftSeed() {
|
|
|
28961
29055
|
description: description,
|
|
28962
29056
|
body: body,
|
|
28963
29057
|
tools: tools,
|
|
28964
|
-
note:
|
|
29058
|
+
note: deps.length ? ('Seeded from ' + deps.length + ' starting point' + (deps.length === 1 ? '' : 's') + '.') : 'Seeded from the natural language description.',
|
|
28965
29059
|
};
|
|
28966
29060
|
}
|
|
28967
29061
|
|
|
29062
|
+
function renderSkillComposerPreview() {
|
|
29063
|
+
var box = document.getElementById('skill-composer-preview');
|
|
29064
|
+
if (!box) return;
|
|
29065
|
+
var text = ((document.getElementById('skill-composer-text') || {}).value || '').trim();
|
|
29066
|
+
var deps = getSkillComposerDependencies(true);
|
|
29067
|
+
if (!text && deps.length === 0) {
|
|
29068
|
+
box.innerHTML = '<span style="color:var(--text-muted)">Preview appears here as you describe the skill or add tools.</span>';
|
|
29069
|
+
return;
|
|
29070
|
+
}
|
|
29071
|
+
var seed = buildSkillComposerDraftSeed();
|
|
29072
|
+
var depHtml = deps.length
|
|
29073
|
+
? deps.map(function(dep) {
|
|
29074
|
+
var copy = _skillComposerCopy[dep.mode] || _skillComposerCopy.outcome;
|
|
29075
|
+
return '<span style="display:inline-flex;margin:0 4px 4px 0;padding:1px 6px;border:1px solid var(--border);border-radius:999px;background:var(--bg-secondary);font-size:10px"><strong style="color:var(--accent);margin-right:4px">' + esc(copy.promptLabel) + '</strong>' + esc(dep.value) + '</span>';
|
|
29076
|
+
}).join('')
|
|
29077
|
+
: '<span style="color:var(--text-muted)">No explicit dependencies yet.</span>';
|
|
29078
|
+
var toolsHtml = seed.tools.length
|
|
29079
|
+
? seed.tools.map(function(t) { return '<code style="font-size:10px;background:var(--bg-secondary);border:1px solid var(--border);padding:1px 5px;border-radius:4px;color:var(--text-primary);margin-right:4px">' + esc(t) + '</code>'; }).join('')
|
|
29080
|
+
: '<span style="color:var(--text-muted)">inherits default runtime surface</span>';
|
|
29081
|
+
box.innerHTML =
|
|
29082
|
+
'<div style="font-weight:600;color:var(--text-primary);margin-bottom:4px">' + esc(seed.title) + '</div>' +
|
|
29083
|
+
'<div style="color:var(--text-muted);margin-bottom:8px">' + esc(seed.name) + '</div>' +
|
|
29084
|
+
'<div style="margin-bottom:8px"><strong style="color:var(--text-secondary)">Dependencies</strong><br>' + depHtml + '</div>' +
|
|
29085
|
+
'<div style="margin-bottom:8px"><strong style="color:var(--text-secondary)">Tools</strong><br>' + toolsHtml + '</div>' +
|
|
29086
|
+
'<div><strong style="color:var(--text-secondary)">Trigger description</strong><br>' + esc(seed.description.slice(0, 260)) + (seed.description.length > 260 ? '...' : '') + '</div>';
|
|
29087
|
+
}
|
|
29088
|
+
|
|
28968
29089
|
async function hydrateSkillComposerOptions() {
|
|
28969
29090
|
if (_skillComposerOptionsCache) return _skillComposerOptionsCache;
|
|
28970
29091
|
var cache = { tool: [], cli: [], project: [], memory: [] };
|
|
@@ -29062,15 +29183,21 @@ async function renderSkillComposerOptions() {
|
|
|
29062
29183
|
|
|
29063
29184
|
function buildSkillComposerPrompt() {
|
|
29064
29185
|
var text = ((document.getElementById('skill-composer-text') || {}).value || '').trim();
|
|
29065
|
-
var
|
|
29066
|
-
var modeCopy = _skillComposerCopy[_skillComposerMode] || _skillComposerCopy.outcome;
|
|
29186
|
+
var deps = getSkillComposerDependencies(true);
|
|
29067
29187
|
var lines = [
|
|
29068
29188
|
'Use skill-creator principles to draft a Clementine skill in Skill Studio.',
|
|
29069
29189
|
'',
|
|
29070
29190
|
'Outcome: ' + (text || '(ask me for the outcome before drafting)'),
|
|
29071
|
-
'Starting
|
|
29191
|
+
'Starting points:',
|
|
29072
29192
|
];
|
|
29073
|
-
if (
|
|
29193
|
+
if (deps.length) {
|
|
29194
|
+
for (var i = 0; i < deps.length; i++) {
|
|
29195
|
+
var copy = _skillComposerCopy[deps[i].mode] || _skillComposerCopy.outcome;
|
|
29196
|
+
lines.push('- ' + copy.promptLabel + ': ' + deps[i].value);
|
|
29197
|
+
}
|
|
29198
|
+
} else {
|
|
29199
|
+
lines.push('- None selected');
|
|
29200
|
+
}
|
|
29074
29201
|
lines.push(
|
|
29075
29202
|
'',
|
|
29076
29203
|
'Draft rules:',
|
|
@@ -34514,6 +34641,54 @@ function renderMd(text) {
|
|
|
34514
34641
|
return s;
|
|
34515
34642
|
}
|
|
34516
34643
|
|
|
34644
|
+
function extractBackgroundOfferId(text) {
|
|
34645
|
+
var m = String(text || '').match(/Background offer:\s*(bo-[a-z0-9]+-[a-z0-9]{3,10})/i);
|
|
34646
|
+
return m ? m[1] : '';
|
|
34647
|
+
}
|
|
34648
|
+
|
|
34649
|
+
function appendBackgroundOfferActions(bubble, text) {
|
|
34650
|
+
if (!bubble || bubble.querySelector('.chat-bg-offer-actions')) return;
|
|
34651
|
+
var id = extractBackgroundOfferId(text);
|
|
34652
|
+
if (!id) return;
|
|
34653
|
+
var panel = document.createElement('div');
|
|
34654
|
+
panel.className = 'chat-bg-offer-actions';
|
|
34655
|
+
panel.style.cssText = 'margin-top:10px;display:flex;gap:6px;flex-wrap:wrap;padding-top:8px;border-top:1px solid var(--border)';
|
|
34656
|
+
panel.innerHTML =
|
|
34657
|
+
'<button class="btn-sm btn-primary" data-bg-action="accept" style="font-size:11px;padding:5px 10px">Run in background</button>' +
|
|
34658
|
+
'<button class="btn-sm" data-bg-action="inline" style="font-size:11px;padding:5px 10px">Run inline anyway</button>' +
|
|
34659
|
+
'<button class="btn-sm" data-bg-action="skill" style="font-size:11px;padding:5px 10px">Save as skill first</button>' +
|
|
34660
|
+
'<button class="btn-sm" data-bg-action="dismiss" style="font-size:11px;padding:5px 10px;color:var(--text-muted)">Dismiss</button>';
|
|
34661
|
+
panel.querySelector('[data-bg-action="accept"]').onclick = function() { acceptBackgroundOffer(id, panel); };
|
|
34662
|
+
panel.querySelector('[data-bg-action="inline"]').onclick = function() { askClementineWith('run inline ' + id); };
|
|
34663
|
+
panel.querySelector('[data-bg-action="skill"]').onclick = function() { askClementineWith('save skill ' + id); };
|
|
34664
|
+
panel.querySelector('[data-bg-action="dismiss"]').onclick = function() { dismissBackgroundOffer(id, panel); };
|
|
34665
|
+
bubble.appendChild(panel);
|
|
34666
|
+
}
|
|
34667
|
+
|
|
34668
|
+
async function acceptBackgroundOffer(id, panel) {
|
|
34669
|
+
if (panel) panel.innerHTML = '<span style="font-size:11px;color:var(--text-muted)">Queueing background task...</span>';
|
|
34670
|
+
try {
|
|
34671
|
+
var r = await apiFetch('/api/background-offers/' + encodeURIComponent(id) + '/accept', { method: 'POST' });
|
|
34672
|
+
var d = await r.json();
|
|
34673
|
+
if (!r.ok || !d.ok) {
|
|
34674
|
+
if (panel) panel.innerHTML = '<span style="font-size:11px;color:var(--red)">' + esc(d.error || 'Failed to queue task') + '</span>';
|
|
34675
|
+
return;
|
|
34676
|
+
}
|
|
34677
|
+
if (panel) panel.innerHTML = '<div style="font-size:12px;line-height:1.45;color:var(--text-secondary)">' + renderMd(d.response || 'Background task queued.') + '</div>';
|
|
34678
|
+
toast('Background task queued', 'success');
|
|
34679
|
+
setTimeout(refreshAll, 500);
|
|
34680
|
+
} catch (err) {
|
|
34681
|
+
if (panel) panel.innerHTML = '<span style="font-size:11px;color:var(--red)">Failed: ' + esc(String(err)) + '</span>';
|
|
34682
|
+
}
|
|
34683
|
+
}
|
|
34684
|
+
|
|
34685
|
+
async function dismissBackgroundOffer(id, panel) {
|
|
34686
|
+
try {
|
|
34687
|
+
await apiFetch('/api/background-offers/' + encodeURIComponent(id) + '/dismiss', { method: 'POST' });
|
|
34688
|
+
} catch (_) { /* best-effort */ }
|
|
34689
|
+
if (panel) panel.remove();
|
|
34690
|
+
}
|
|
34691
|
+
|
|
34517
34692
|
let chatHistory = [];
|
|
34518
34693
|
async function sendChat() {
|
|
34519
34694
|
const input = document.getElementById('chat-input');
|
|
@@ -34621,6 +34796,7 @@ async function sendChat() {
|
|
|
34621
34796
|
finalText = evt.response || finalText || 'No response';
|
|
34622
34797
|
finalTrace = evt.trace || null;
|
|
34623
34798
|
renderAssistantText(finalText);
|
|
34799
|
+
appendBackgroundOfferActions(asstBubble, finalText);
|
|
34624
34800
|
} else if (evt.type === 'error') {
|
|
34625
34801
|
throw new Error(evt.error || 'Stream error');
|
|
34626
34802
|
}
|
package/dist/gateway/router.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Manages per-user/channel sessions for conversation continuity.
|
|
6
6
|
*/
|
|
7
7
|
import { PersonalAssistant, type ProjectMeta } from '../agent/assistant.js';
|
|
8
|
-
import type { OnProgressCallback, OnTextCallback, OnToolActivityCallback, SelfImproveConfig, SelfImproveExperiment, SessionProvenance, TeamMessage, VerboseLevel, WorkflowDefinition } from '../types.js';
|
|
8
|
+
import type { BackgroundTask, OnProgressCallback, OnTextCallback, OnToolActivityCallback, SelfImproveConfig, SelfImproveExperiment, SessionProvenance, TeamMessage, VerboseLevel, WorkflowDefinition } from '../types.js';
|
|
9
9
|
import { AgentManager } from '../agent/agent-manager.js';
|
|
10
10
|
import { TeamRouter } from '../agent/team-router.js';
|
|
11
11
|
import { TeamBus } from '../agent/team-bus.js';
|
|
@@ -23,6 +23,7 @@ export declare class Gateway {
|
|
|
23
23
|
private approvalResolvers;
|
|
24
24
|
private approvalCounter;
|
|
25
25
|
private sessions;
|
|
26
|
+
private pendingBackgroundOffers;
|
|
26
27
|
private auditLog;
|
|
27
28
|
private draining;
|
|
28
29
|
/** Side-channel for the metadata of the *most recent* cron run, populated
|
|
@@ -53,6 +54,25 @@ export declare class Gateway {
|
|
|
53
54
|
private isTrustedPersonalSession;
|
|
54
55
|
private runningUnleashedTasks;
|
|
55
56
|
private extractBackgroundTaskId;
|
|
57
|
+
private makeBackgroundOfferId;
|
|
58
|
+
private backgroundAgentForSession;
|
|
59
|
+
private pruneExpiredBackgroundOffers;
|
|
60
|
+
private latestBackgroundOfferForSession;
|
|
61
|
+
private getBackgroundOfferForSession;
|
|
62
|
+
private createBackgroundOffer;
|
|
63
|
+
private queueBackgroundOffer;
|
|
64
|
+
private formatBackgroundQueuedResponse;
|
|
65
|
+
private formatBackgroundOfferResponse;
|
|
66
|
+
acceptBackgroundOffer(sessionKey: string, id: string): {
|
|
67
|
+
ok: boolean;
|
|
68
|
+
response: string;
|
|
69
|
+
task?: BackgroundTask;
|
|
70
|
+
};
|
|
71
|
+
dismissBackgroundOffer(sessionKey: string, id: string): {
|
|
72
|
+
ok: boolean;
|
|
73
|
+
response: string;
|
|
74
|
+
};
|
|
75
|
+
private resolveBackgroundOfferControl;
|
|
56
76
|
private isAgentScopedSession;
|
|
57
77
|
private readUnleashedStatus;
|
|
58
78
|
private taskElapsedMinutes;
|
package/dist/gateway/router.js
CHANGED
|
@@ -16,7 +16,7 @@ import { lanes } from './lanes.js';
|
|
|
16
16
|
import { AgentManager } from '../agent/agent-manager.js';
|
|
17
17
|
import { TeamRouter } from '../agent/team-router.js';
|
|
18
18
|
import { TeamBus } from '../agent/team-bus.js';
|
|
19
|
-
import { listBackgroundTasks, loadBackgroundTask, markFailed } from '../agent/background-tasks.js';
|
|
19
|
+
import { createBackgroundTask, listBackgroundTasks, loadBackgroundTask, markFailed } from '../agent/background-tasks.js';
|
|
20
20
|
import { applyAssistantExperienceUpdate, detectApprovalReply, detectLocalTurn } from '../agent/local-turn.js';
|
|
21
21
|
import { buildApprovalFollowupPrompt, detectActionExpectation } from '../agent/action-enforcer.js';
|
|
22
22
|
import { updateClementineJson } from '../config/clementine-json.js';
|
|
@@ -36,6 +36,7 @@ import { isLiveUnleashedStatus } from './unleashed-status.js';
|
|
|
36
36
|
import { buildActiveContextSnapshot } from './active-context.js';
|
|
37
37
|
import { markContextEventBySource } from './context-events.js';
|
|
38
38
|
import { EventLog } from './event-log.js';
|
|
39
|
+
import { detectComplexTaskForBackground } from '../agent/complex-task-detector.js';
|
|
39
40
|
export { isLiveUnleashedStatus } from './unleashed-status.js';
|
|
40
41
|
const logger = pino({ name: 'clementine.gateway' });
|
|
41
42
|
const INTERACTIVE_FAILURE_LOG = path.join(BASE_DIR, 'self-improve', 'interactive-failures.jsonl');
|
|
@@ -130,6 +131,7 @@ export class Gateway {
|
|
|
130
131
|
approvalResolvers = new Map();
|
|
131
132
|
approvalCounter = 0;
|
|
132
133
|
sessions = new Map();
|
|
134
|
+
pendingBackgroundOffers = new Map();
|
|
133
135
|
auditLog = [];
|
|
134
136
|
draining = false;
|
|
135
137
|
/** Side-channel for the metadata of the *most recent* cron run, populated
|
|
@@ -245,15 +247,157 @@ export class Gateway {
|
|
|
245
247
|
extractBackgroundTaskId(text) {
|
|
246
248
|
return text.match(BACKGROUND_TASK_ID_RE)?.[0]?.toLowerCase();
|
|
247
249
|
}
|
|
250
|
+
makeBackgroundOfferId() {
|
|
251
|
+
return `bo-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
252
|
+
}
|
|
253
|
+
backgroundAgentForSession(sessionKey) {
|
|
254
|
+
return this._agentSlugFromSessionKey(sessionKey) ?? this.getSessionProfile(sessionKey) ?? 'clementine';
|
|
255
|
+
}
|
|
256
|
+
pruneExpiredBackgroundOffers() {
|
|
257
|
+
const now = Date.now();
|
|
258
|
+
for (const [id, offer] of this.pendingBackgroundOffers) {
|
|
259
|
+
if (offer.expiresAt <= now)
|
|
260
|
+
this.pendingBackgroundOffers.delete(id);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
latestBackgroundOfferForSession(sessionKey) {
|
|
264
|
+
this.pruneExpiredBackgroundOffers();
|
|
265
|
+
const offers = [...this.pendingBackgroundOffers.values()]
|
|
266
|
+
.filter((offer) => offer.sessionKey === sessionKey)
|
|
267
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
268
|
+
return offers[0];
|
|
269
|
+
}
|
|
270
|
+
getBackgroundOfferForSession(sessionKey, id) {
|
|
271
|
+
this.pruneExpiredBackgroundOffers();
|
|
272
|
+
const offer = this.pendingBackgroundOffers.get(id);
|
|
273
|
+
if (!offer || offer.sessionKey !== sessionKey)
|
|
274
|
+
return undefined;
|
|
275
|
+
return offer;
|
|
276
|
+
}
|
|
277
|
+
createBackgroundOffer(sessionKey, prompt, recommendation) {
|
|
278
|
+
this.pruneExpiredBackgroundOffers();
|
|
279
|
+
const offer = {
|
|
280
|
+
id: this.makeBackgroundOfferId(),
|
|
281
|
+
sessionKey,
|
|
282
|
+
fromAgent: this.backgroundAgentForSession(sessionKey),
|
|
283
|
+
prompt,
|
|
284
|
+
recommendation,
|
|
285
|
+
createdAt: Date.now(),
|
|
286
|
+
expiresAt: Date.now() + 30 * 60_000,
|
|
287
|
+
};
|
|
288
|
+
this.pendingBackgroundOffers.set(offer.id, offer);
|
|
289
|
+
return offer;
|
|
290
|
+
}
|
|
291
|
+
queueBackgroundOffer(offer) {
|
|
292
|
+
const task = createBackgroundTask({
|
|
293
|
+
fromAgent: offer.fromAgent,
|
|
294
|
+
prompt: offer.prompt,
|
|
295
|
+
maxMinutes: offer.recommendation.suggestedMaxMinutes,
|
|
296
|
+
sessionKey: offer.sessionKey,
|
|
297
|
+
});
|
|
298
|
+
this.pendingBackgroundOffers.delete(offer.id);
|
|
299
|
+
logger.info({
|
|
300
|
+
offerId: offer.id,
|
|
301
|
+
taskId: task.id,
|
|
302
|
+
sessionKey: offer.sessionKey,
|
|
303
|
+
fromAgent: offer.fromAgent,
|
|
304
|
+
maxMinutes: task.maxMinutes,
|
|
305
|
+
}, 'Queued background task from chat offer');
|
|
306
|
+
return task;
|
|
307
|
+
}
|
|
308
|
+
formatBackgroundQueuedResponse(task) {
|
|
309
|
+
return [
|
|
310
|
+
`Queued background task **${task.id}**.`,
|
|
311
|
+
'',
|
|
312
|
+
`It will run as **${task.fromAgent}** with a ${task.maxMinutes} minute cap.`,
|
|
313
|
+
`Use \`status ${task.id}\` or check the dashboard Background Tasks panel for progress.`,
|
|
314
|
+
].join('\n');
|
|
315
|
+
}
|
|
316
|
+
formatBackgroundOfferResponse(offer) {
|
|
317
|
+
const lines = [
|
|
318
|
+
'This looks like long-running, multi-tool work. I recommend running it in the background so chat does not go stale.',
|
|
319
|
+
'',
|
|
320
|
+
'**Plan**',
|
|
321
|
+
...offer.recommendation.plan.map((step, idx) => `${idx + 1}. ${step}`),
|
|
322
|
+
'',
|
|
323
|
+
`**Why background:** ${offer.recommendation.reasons.join('; ')}.`,
|
|
324
|
+
`**Estimated cap:** ${offer.recommendation.suggestedMaxMinutes} minutes.`,
|
|
325
|
+
`**Background offer:** ${offer.id}`,
|
|
326
|
+
'',
|
|
327
|
+
`Reply \`run background ${offer.id}\` to queue it, \`run inline ${offer.id}\` to run it in this chat, or \`save skill ${offer.id}\` to make it reusable first.`,
|
|
328
|
+
];
|
|
329
|
+
return lines.join('\n');
|
|
330
|
+
}
|
|
331
|
+
acceptBackgroundOffer(sessionKey, id) {
|
|
332
|
+
const offer = this.getBackgroundOfferForSession(sessionKey, id);
|
|
333
|
+
if (!offer) {
|
|
334
|
+
return { ok: false, response: `I could not find an active background offer for ${id}. It may have expired.` };
|
|
335
|
+
}
|
|
336
|
+
const task = this.queueBackgroundOffer(offer);
|
|
337
|
+
return { ok: true, response: this.formatBackgroundQueuedResponse(task), task };
|
|
338
|
+
}
|
|
339
|
+
dismissBackgroundOffer(sessionKey, id) {
|
|
340
|
+
const offer = this.getBackgroundOfferForSession(sessionKey, id);
|
|
341
|
+
if (!offer)
|
|
342
|
+
return { ok: false, response: `No active background offer found for ${id}.` };
|
|
343
|
+
this.pendingBackgroundOffers.delete(id);
|
|
344
|
+
return { ok: true, response: `Dismissed background offer ${id}.` };
|
|
345
|
+
}
|
|
346
|
+
resolveBackgroundOfferControl(sessionKey, text) {
|
|
347
|
+
const normalized = text.trim().toLowerCase();
|
|
348
|
+
const explicitOfferId = text.match(/\bbo-[a-z0-9]+-[a-z0-9]{3,10}\b/i)?.[0]?.toLowerCase();
|
|
349
|
+
const offer = explicitOfferId
|
|
350
|
+
? this.getBackgroundOfferForSession(sessionKey, explicitOfferId)
|
|
351
|
+
: this.latestBackgroundOfferForSession(sessionKey);
|
|
352
|
+
if (!offer)
|
|
353
|
+
return null;
|
|
354
|
+
if (/^(run|start|queue|approve|yes|go|do it).{0,30}\bbackground\b/i.test(normalized)
|
|
355
|
+
|| /^run it in the background\b/i.test(normalized)
|
|
356
|
+
|| /^background\b/i.test(normalized)
|
|
357
|
+
|| /^(yes|yep|approved?|go|do it|please do|start it)$/i.test(normalized)) {
|
|
358
|
+
const task = this.queueBackgroundOffer(offer);
|
|
359
|
+
return {
|
|
360
|
+
response: this.formatBackgroundQueuedResponse(task),
|
|
361
|
+
executionMode: 'background_queued',
|
|
362
|
+
backgroundTaskId: task.id,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (/^run.{0,30}\binline\b/i.test(normalized) || /^run inline\b/i.test(normalized)) {
|
|
366
|
+
this.pendingBackgroundOffers.delete(offer.id);
|
|
367
|
+
return { inlineText: offer.prompt, executionMode: 'inline' };
|
|
368
|
+
}
|
|
369
|
+
if (/^(save|create|make|teach).{0,30}\bskill\b/i.test(normalized)) {
|
|
370
|
+
this.pendingBackgroundOffers.delete(offer.id);
|
|
371
|
+
return {
|
|
372
|
+
skillText: [
|
|
373
|
+
'Create a reusable Clementine skill for this workflow using skill-creator principles.',
|
|
374
|
+
'Write a concise Anthropic-compatible folder-form SKILL.md with clear trigger description, required tools/MCP/CLI dependencies, procedure, success criteria, and failure handling.',
|
|
375
|
+
'',
|
|
376
|
+
'Original workflow request:',
|
|
377
|
+
offer.prompt,
|
|
378
|
+
].join('\n'),
|
|
379
|
+
executionMode: 'inline',
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
248
384
|
isAgentScopedSession(sessionKey) {
|
|
249
385
|
return this._agentSlugFromSessionKey(sessionKey) !== undefined;
|
|
250
386
|
}
|
|
251
387
|
readUnleashedStatus(jobName) {
|
|
252
388
|
try {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
389
|
+
const candidates = [
|
|
390
|
+
jobName,
|
|
391
|
+
jobName.startsWith('bg-') ? `bg:${jobName}` : '',
|
|
392
|
+
].filter(Boolean);
|
|
393
|
+
for (const candidate of candidates) {
|
|
394
|
+
const safeJob = candidate.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
395
|
+
const statusPath = path.join(BASE_DIR, 'unleashed', safeJob, 'status.json');
|
|
396
|
+
if (existsSync(statusPath)) {
|
|
397
|
+
return JSON.parse(readFileSync(statusPath, 'utf-8'));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
257
401
|
}
|
|
258
402
|
catch {
|
|
259
403
|
return null;
|
|
@@ -300,10 +444,14 @@ export class Gateway {
|
|
|
300
444
|
: (task.status === 'failed' || task.status === 'aborted') && task.error
|
|
301
445
|
? ` Reason: ${this.taskSummary(task.error, 120)}`
|
|
302
446
|
: '';
|
|
303
|
-
|
|
447
|
+
const deliverable = task.status === 'done' && task.deliverableNote
|
|
448
|
+
? ` Deliverable: ${this.taskSummary(task.deliverableNote, 120)}`
|
|
449
|
+
: '';
|
|
450
|
+
return `- ${task.id}: ${task.status}${phase}, ${elapsed} min${cap}. ${taskText}${terminalDetail}${deliverable}`;
|
|
304
451
|
}
|
|
305
452
|
writeUnleashedCancel(jobName) {
|
|
306
|
-
const
|
|
453
|
+
const safeJob = (jobName.startsWith('bg-') ? `bg:${jobName}` : jobName).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
454
|
+
const cancelDir = path.join(BASE_DIR, 'unleashed', safeJob);
|
|
307
455
|
mkdirSync(cancelDir, { recursive: true });
|
|
308
456
|
writeFileSync(path.join(cancelDir, 'CANCEL'), '');
|
|
309
457
|
}
|
|
@@ -1312,6 +1460,9 @@ export class Gateway {
|
|
|
1312
1460
|
actionExpectationSource: actionExpectationForLedger.source,
|
|
1313
1461
|
actionExpectationReason: actionExpectationForLedger.reason,
|
|
1314
1462
|
runId: ledgerRunMetadata.runId,
|
|
1463
|
+
executionMode: ledgerRunMetadata.executionMode,
|
|
1464
|
+
backgroundTaskId: ledgerRunMetadata.backgroundTaskId,
|
|
1465
|
+
skillsApplied: ledgerRunMetadata.skillsApplied,
|
|
1315
1466
|
permissionModeApplied: ledgerRunMetadata.permissionModeApplied,
|
|
1316
1467
|
allowedToolsApplied: ledgerRunMetadata.allowedToolsApplied?.slice(0, 80),
|
|
1317
1468
|
builtinToolsApplied: ledgerRunMetadata.builtinToolsApplied?.slice(0, 40),
|
|
@@ -1332,7 +1483,8 @@ export class Gateway {
|
|
|
1332
1483
|
});
|
|
1333
1484
|
}
|
|
1334
1485
|
async _handleMessageInner(sessionKey, text, onText, model, maxTurns, onToolActivity, onProgress, ledgerRunMetadata) {
|
|
1335
|
-
|
|
1486
|
+
let originalText = text;
|
|
1487
|
+
let skipBackgroundOffer = false;
|
|
1336
1488
|
// Per-segment latency capture — emitted as a single 'chat:latency' line
|
|
1337
1489
|
// on the happy path so we can grep/aggregate without parsing many lines.
|
|
1338
1490
|
const tInnerStart = Date.now();
|
|
@@ -1438,6 +1590,30 @@ export class Gateway {
|
|
|
1438
1590
|
}, 'chat:latency');
|
|
1439
1591
|
return localResponse;
|
|
1440
1592
|
}
|
|
1593
|
+
const backgroundControl = this.resolveBackgroundOfferControl(sessionKey, text);
|
|
1594
|
+
if (backgroundControl?.response) {
|
|
1595
|
+
if (ledgerRunMetadata) {
|
|
1596
|
+
ledgerRunMetadata.executionMode = backgroundControl.executionMode;
|
|
1597
|
+
ledgerRunMetadata.backgroundTaskId = backgroundControl.backgroundTaskId;
|
|
1598
|
+
}
|
|
1599
|
+
if (onText) {
|
|
1600
|
+
try {
|
|
1601
|
+
await onText(backgroundControl.response);
|
|
1602
|
+
}
|
|
1603
|
+
catch { /* channel streaming is best-effort */ }
|
|
1604
|
+
}
|
|
1605
|
+
return backgroundControl.response;
|
|
1606
|
+
}
|
|
1607
|
+
if (backgroundControl?.inlineText) {
|
|
1608
|
+
text = backgroundControl.inlineText;
|
|
1609
|
+
originalText = backgroundControl.inlineText;
|
|
1610
|
+
skipBackgroundOffer = true;
|
|
1611
|
+
}
|
|
1612
|
+
else if (backgroundControl?.skillText) {
|
|
1613
|
+
text = backgroundControl.skillText;
|
|
1614
|
+
originalText = backgroundControl.skillText;
|
|
1615
|
+
skipBackgroundOffer = true;
|
|
1616
|
+
}
|
|
1441
1617
|
const approvalFollowupExpected = this.isTrustedPersonalSession(sessionKey)
|
|
1442
1618
|
&& detectApprovalReply(originalText) === true
|
|
1443
1619
|
&& this.assistant.hasRecentApprovalPrompt(sessionKey);
|
|
@@ -1685,6 +1861,46 @@ export class Gateway {
|
|
|
1685
1861
|
const resolvedProfile = profileSlug
|
|
1686
1862
|
? this.getAgentManager().get(profileSlug) ?? undefined
|
|
1687
1863
|
: undefined;
|
|
1864
|
+
const isBuilderSession = sessionKey.startsWith('dashboard:builder:');
|
|
1865
|
+
if (!skipBackgroundOffer && !isBuilderSession && !isInternalMsg && this.isTrustedPersonalSession(sessionKey)) {
|
|
1866
|
+
const recommendation = detectComplexTaskForBackground(text);
|
|
1867
|
+
if (recommendation) {
|
|
1868
|
+
const offer = this.createBackgroundOffer(sessionKey, text, recommendation);
|
|
1869
|
+
if (recommendation.queueImmediately) {
|
|
1870
|
+
const task = this.queueBackgroundOffer(offer);
|
|
1871
|
+
const queued = this.formatBackgroundQueuedResponse(task);
|
|
1872
|
+
if (ledgerRunMetadata) {
|
|
1873
|
+
ledgerRunMetadata.executionMode = 'background_queued';
|
|
1874
|
+
ledgerRunMetadata.backgroundTaskId = task.id;
|
|
1875
|
+
}
|
|
1876
|
+
if (onText) {
|
|
1877
|
+
try {
|
|
1878
|
+
await onText(queued);
|
|
1879
|
+
}
|
|
1880
|
+
catch { /* channel streaming is best-effort */ }
|
|
1881
|
+
}
|
|
1882
|
+
return queued;
|
|
1883
|
+
}
|
|
1884
|
+
const offerText = this.formatBackgroundOfferResponse(offer);
|
|
1885
|
+
if (ledgerRunMetadata) {
|
|
1886
|
+
ledgerRunMetadata.executionMode = 'background_offer';
|
|
1887
|
+
}
|
|
1888
|
+
logger.info({
|
|
1889
|
+
sessionKey,
|
|
1890
|
+
offerId: offer.id,
|
|
1891
|
+
score: recommendation.score,
|
|
1892
|
+
reasons: recommendation.reasons,
|
|
1893
|
+
maxMinutes: recommendation.suggestedMaxMinutes,
|
|
1894
|
+
}, 'Offering background execution for complex chat request');
|
|
1895
|
+
if (onText) {
|
|
1896
|
+
try {
|
|
1897
|
+
await onText(offerText);
|
|
1898
|
+
}
|
|
1899
|
+
catch { /* channel streaming is best-effort */ }
|
|
1900
|
+
}
|
|
1901
|
+
return offerText;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1688
1904
|
const hygiene = assessGatewayContextHygiene({
|
|
1689
1905
|
sessionKey: effectiveSessionKey,
|
|
1690
1906
|
textChars: enrichedText.length,
|
|
@@ -1804,7 +2020,6 @@ export class Gateway {
|
|
|
1804
2020
|
// the agent only emits json-artifact blocks. Strip everything
|
|
1805
2021
|
// expensive; keep just SDK session resume so multi-turn
|
|
1806
2022
|
// artifact iteration sees its own prior turns.
|
|
1807
|
-
const isBuilderSession = sessionKey.startsWith('dashboard:builder:');
|
|
1808
2023
|
// ── Skill auto-match (1.18.170) ─────────────────────────────
|
|
1809
2024
|
// Match the user's message against the skill catalog (auto-
|
|
1810
2025
|
// skills + user-authored). Top-3 matches above score ≥ 4 inform:
|
|
@@ -1903,7 +2118,6 @@ export class Gateway {
|
|
|
1903
2118
|
...(chatMcp ? { extraMcpServers: chatMcp.servers } : {}),
|
|
1904
2119
|
onText: wrappedOnText,
|
|
1905
2120
|
onToolActivity: ({ tool, input }) => {
|
|
1906
|
-
toolActivityCount++;
|
|
1907
2121
|
if (wrappedOnToolActivity) {
|
|
1908
2122
|
return wrappedOnToolActivity(tool, input);
|
|
1909
2123
|
}
|
|
@@ -1913,6 +2127,12 @@ export class Gateway {
|
|
|
1913
2127
|
});
|
|
1914
2128
|
if (ledgerRunMetadata) {
|
|
1915
2129
|
ledgerRunMetadata.runId = runAgentResult.runId;
|
|
2130
|
+
ledgerRunMetadata.executionMode = ledgerRunMetadata.executionMode ?? 'inline';
|
|
2131
|
+
ledgerRunMetadata.skillsApplied = resolvedSkills?.matches.map((m) => ({
|
|
2132
|
+
name: m.name,
|
|
2133
|
+
source: 'auto',
|
|
2134
|
+
score: m.score,
|
|
2135
|
+
}));
|
|
1916
2136
|
ledgerRunMetadata.permissionModeApplied = runAgentResult.permissionMode;
|
|
1917
2137
|
ledgerRunMetadata.allowedToolsApplied = runAgentResult.allowedToolsApplied;
|
|
1918
2138
|
ledgerRunMetadata.builtinToolsApplied = runAgentResult.builtinToolsApplied;
|
|
@@ -17,6 +17,13 @@ export interface TurnLedgerEntry {
|
|
|
17
17
|
actionExpectationSource?: string;
|
|
18
18
|
actionExpectationReason?: string;
|
|
19
19
|
runId?: string;
|
|
20
|
+
executionMode?: 'inline' | 'background_offer' | 'background_queued';
|
|
21
|
+
backgroundTaskId?: string;
|
|
22
|
+
skillsApplied?: Array<{
|
|
23
|
+
name: string;
|
|
24
|
+
source: 'auto' | 'pinned';
|
|
25
|
+
score?: number;
|
|
26
|
+
}>;
|
|
20
27
|
permissionModeApplied?: string;
|
|
21
28
|
allowedToolsApplied?: string[];
|
|
22
29
|
builtinToolsApplied?: string[];
|
|
@@ -40,7 +40,12 @@ export function formatLastTurnLedger(sessionKey, baseDir = BASE_DIR) {
|
|
|
40
40
|
? `Tools used: ${last.toolCallsMade} (${last.toolNames.slice(0, 6).join(', ')}${last.toolNames.length > 6 ? ', ...' : ''}).`
|
|
41
41
|
: 'Tools used: none.';
|
|
42
42
|
const execution = last.permissionModeApplied || last.mcpServersApplied?.length
|
|
43
|
-
? `Execution: ${last.permissionModeApplied ?? 'unknown'}${last.mcpServersApplied?.length ? `; MCP: ${last.mcpServersApplied.slice(0, 6).join(', ')}${last.mcpServersApplied.length > 6 ? ', ...' : ''}` : ''}.`
|
|
43
|
+
? `Execution: ${last.executionMode ?? 'inline'}; ${last.permissionModeApplied ?? 'unknown'}${last.mcpServersApplied?.length ? `; MCP: ${last.mcpServersApplied.slice(0, 6).join(', ')}${last.mcpServersApplied.length > 6 ? ', ...' : ''}` : ''}.`
|
|
44
|
+
: last.executionMode
|
|
45
|
+
? `Execution: ${last.executionMode}${last.backgroundTaskId ? ` (${last.backgroundTaskId})` : ''}.`
|
|
46
|
+
: '';
|
|
47
|
+
const skills = last.skillsApplied?.length
|
|
48
|
+
? `Skills: ${last.skillsApplied.slice(0, 5).map((s) => s.name).join(', ')}${last.skillsApplied.length > 5 ? ', ...' : ''}.`
|
|
44
49
|
: '';
|
|
45
50
|
const response = last.responsePreview
|
|
46
51
|
? `Last response: "${last.responsePreview.replace(/\s+/g, ' ').slice(0, 240)}${last.responsePreview.length > 240 ? '...' : ''}"`
|
|
@@ -54,6 +59,7 @@ export function formatLastTurnLedger(sessionKey, baseDir = BASE_DIR) {
|
|
|
54
59
|
`Toolset: ${last.toolset ?? 'auto'}.`,
|
|
55
60
|
`Policy: ${last.policyReason ?? 'unknown'}; tools ${last.toolsEnabled ? 'enabled' : 'disabled'}.`,
|
|
56
61
|
execution,
|
|
62
|
+
skills,
|
|
57
63
|
response,
|
|
58
64
|
].filter(Boolean).join('\n');
|
|
59
65
|
}
|
|
@@ -65,6 +65,10 @@ export function registerBackgroundTaskTools(server) {
|
|
|
65
65
|
lines.push('');
|
|
66
66
|
lines.push(`Result:\n${task.result}`);
|
|
67
67
|
}
|
|
68
|
+
if (task.resultPath) {
|
|
69
|
+
lines.push('');
|
|
70
|
+
lines.push(`Full result: ${task.resultPath}`);
|
|
71
|
+
}
|
|
68
72
|
if (task.deliverableNote) {
|
|
69
73
|
lines.push('');
|
|
70
74
|
lines.push(`Deliverable: ${task.deliverableNote}`);
|
package/dist/types.d.ts
CHANGED