codeply-cli 0.5.0 → 0.5.1
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/lib/bots.js +413 -1
- package/lib/cloud.mjs +45 -6
- package/package.json +1 -1
package/lib/bots.js
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* for a chat, buildBotPrompt() is added to that run's system prompt. After
|
|
7
7
|
* each reply, learnFromTurn() asks the model for at most 3 durable facts to
|
|
8
8
|
* remember, so the bot gets better at working with this user over time.
|
|
9
|
+
* After a run that used tools, reflectOnRun() looks at what the bot did and
|
|
10
|
+
* keeps experience (lessons, playbooks, tool tips, open threads), so the bot
|
|
11
|
+
* also gets better at the work itself.
|
|
9
12
|
*
|
|
10
13
|
* Bots talk to each other through the ask_bot tool. delegate() runs the asked
|
|
11
14
|
* bot as an isolated sub-run (its own prompt, only the task text it was
|
|
@@ -23,6 +26,12 @@ const path = require('path');
|
|
|
23
26
|
const MAX_MEMORY = 60;
|
|
24
27
|
const MAX_FACTS_PER_TURN = 3;
|
|
25
28
|
const MAX_DEPTH = 2;
|
|
29
|
+
// Experience memory (MUSE style: learned from the bot's own work, not about the user).
|
|
30
|
+
const MAX_LESSONS = 12;
|
|
31
|
+
const MAX_PLAYBOOKS = 40;
|
|
32
|
+
const MAX_TOOL_TIPS = 15;
|
|
33
|
+
const MAX_OPEN_THREADS = 5;
|
|
34
|
+
const PLAYBOOKS_IN_FULL = 2;
|
|
26
35
|
|
|
27
36
|
let dirOverride = null;
|
|
28
37
|
function botsDir() {
|
|
@@ -191,7 +200,14 @@ function normalizeBot(input, existing) {
|
|
|
191
200
|
sources: clip(b.sources, 2000),
|
|
192
201
|
approval: Array.isArray(b.approval) ? [...new Set(b.approval.filter((k) => APPROVALS[k]))] : ALL_APPROVALS.slice(),
|
|
193
202
|
avatar: normalizeAvatar(b.avatar),
|
|
203
|
+
// The voice it speaks with on calls (a Deepgram, Edge or Kokoro voice id; '' = pick one).
|
|
204
|
+
voice: typeof b.voice === 'string' && /^[\w.-]{0,80}$/.test(b.voice) ? b.voice : '',
|
|
194
205
|
memory: cleanMemory(b.memory),
|
|
206
|
+
// Experience (learned on the job, see reflectOnRun): lessons, playbooks, tool tips, open threads.
|
|
207
|
+
lessons: cleanLessons(b.lessons),
|
|
208
|
+
playbooks: cleanPlaybooks(b.playbooks),
|
|
209
|
+
toolTips: cleanTips(b.toolTips),
|
|
210
|
+
openThreads: cleanThreads(b.openThreads),
|
|
195
211
|
template: typeof b.template === 'string' ? b.template : undefined,
|
|
196
212
|
createdAt: existing ? existing.createdAt : (Number(b.createdAt) || Date.now()),
|
|
197
213
|
updatedAt: Date.now(),
|
|
@@ -353,6 +369,261 @@ Respond with ONLY a JSON object {"facts": ["..."]}. Use an empty list when nothi
|
|
|
353
369
|
}
|
|
354
370
|
}
|
|
355
371
|
|
|
372
|
+
// ─── Experience: learning on the job ────────────────────────────────────────
|
|
373
|
+
// Memory (above) is about the USER. Experience is about the WORK, after MUSE
|
|
374
|
+
// ("Learning on the Job", arxiv 2510.08002): after a run that used tools, the
|
|
375
|
+
// bot reflects once on what it did (reflectOnRun) and keeps
|
|
376
|
+
// - lessons: short <situation, what works> pairs, always in its prompt
|
|
377
|
+
// - playbooks: named procedures for work that succeeded (app / task, when
|
|
378
|
+
// to use, steps with the real tool names, precautions); a
|
|
379
|
+
// one-line index is always in the prompt, the ones that fit
|
|
380
|
+
// the current request go in full
|
|
381
|
+
// - toolTips: how tools behaved for this bot and user
|
|
382
|
+
// - openThreads: what it left unfinished, until a later run finishes it
|
|
383
|
+
// All bounded, deduplicated, and stored on the bot file like memory.
|
|
384
|
+
|
|
385
|
+
const SECRET_RE = /(api[_ -]?key|password|passwd|secret|token|bearer)\s*[:=]|\bsk-[a-z0-9]{8,}/i;
|
|
386
|
+
const noSecret = (s) => !SECRET_RE.test(String(s || ''));
|
|
387
|
+
const cleanList = (v, max, n) => (Array.isArray(v) ? v : [])
|
|
388
|
+
.filter((s) => typeof s === 'string' && s.trim() && noSecret(s))
|
|
389
|
+
.map((s) => clip(s, n)).slice(0, max);
|
|
390
|
+
|
|
391
|
+
function cleanLessons(list) {
|
|
392
|
+
return (Array.isArray(list) ? list : [])
|
|
393
|
+
.filter((l) => l && typeof l.situation === 'string' && typeof l.strategy === 'string' && l.situation.trim() && l.strategy.trim())
|
|
394
|
+
.map((l) => ({ situation: clip(l.situation, 160), strategy: clip(l.strategy, 240), at: Number(l.at) || Date.now() }))
|
|
395
|
+
.slice(-MAX_LESSONS);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function cleanPlaybook(p) {
|
|
399
|
+
if (!p || typeof p !== 'object') return null;
|
|
400
|
+
const app = clip(p.app, 40);
|
|
401
|
+
const task = clip(p.task, 80);
|
|
402
|
+
const steps = cleanList(p.steps, 10, 200);
|
|
403
|
+
if (!app || !task || !steps.length) return null;
|
|
404
|
+
return {
|
|
405
|
+
app, task, when: clip(p.when, 200), steps,
|
|
406
|
+
precautions: cleanList(p.precautions, 6, 200),
|
|
407
|
+
params: noSecret(p.params) ? clip(p.params, 300) : '',
|
|
408
|
+
uses: Math.max(1, Number(p.uses) || 1),
|
|
409
|
+
at: Number(p.at) || Date.now(),
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function cleanPlaybooks(list) {
|
|
414
|
+
return (Array.isArray(list) ? list : []).map(cleanPlaybook).filter(Boolean).slice(-MAX_PLAYBOOKS);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function cleanTips(list) {
|
|
418
|
+
return (Array.isArray(list) ? list : [])
|
|
419
|
+
.map((t) => (typeof t === 'string' ? { tip: t } : t))
|
|
420
|
+
.filter((t) => t && typeof t.tip === 'string' && t.tip.trim())
|
|
421
|
+
.map((t) => ({ tip: clip(t.tip, 200), at: Number(t.at) || Date.now() }))
|
|
422
|
+
.slice(-MAX_TOOL_TIPS);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function cleanThreads(list) {
|
|
426
|
+
return (Array.isArray(list) ? list : [])
|
|
427
|
+
.map((t) => (typeof t === 'string' ? { text: t } : t))
|
|
428
|
+
.filter((t) => t && typeof t.text === 'string' && t.text.trim())
|
|
429
|
+
.map((t) => ({ text: clip(t.text, 200), at: Number(t.at) || Date.now() }))
|
|
430
|
+
.slice(-MAX_OPEN_THREADS);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const playbookKey = (p) => `${factKey(p.app)}|${factKey(p.task)}`;
|
|
434
|
+
const playbookName = (p) => `${p.app} / ${p.task}`;
|
|
435
|
+
const uniqueStrings = (list, max) => {
|
|
436
|
+
const out = [];
|
|
437
|
+
for (const s of list) if (!out.some((x) => sameFact(x, s))) out.push(s);
|
|
438
|
+
return out.slice(-max);
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Pure merge of one reflection into a bot's experience. Same lesson
|
|
443
|
+
* situation, same tool tip, same app + task: the newer one replaces (a
|
|
444
|
+
* playbook is updated in place, keeping its use count and precautions).
|
|
445
|
+
* @returns {{experience:object, added:object}}
|
|
446
|
+
*/
|
|
447
|
+
function mergeExperience(exp, r, now = Date.now()) {
|
|
448
|
+
let lessons = cleanLessons(exp && exp.lessons);
|
|
449
|
+
let playbooks = cleanPlaybooks(exp && exp.playbooks);
|
|
450
|
+
let toolTips = cleanTips(exp && exp.toolTips);
|
|
451
|
+
let openThreads = cleanThreads(exp && exp.openThreads);
|
|
452
|
+
const added = { lessons: [], toolTips: [], playbook: null, updated: false, unfinished: '', resolved: [] };
|
|
453
|
+
for (const l of cleanLessons((r && r.lessons) || [])) {
|
|
454
|
+
if (!noSecret(l.situation + l.strategy)) continue;
|
|
455
|
+
lessons = lessons.filter((x) => !sameFact(x.situation, l.situation) && !sameFact(x.strategy, l.strategy));
|
|
456
|
+
lessons.push({ ...l, at: now });
|
|
457
|
+
added.lessons.push(l);
|
|
458
|
+
}
|
|
459
|
+
for (const t of cleanTips(((r && r.toolTips) || []).filter(noSecret))) {
|
|
460
|
+
toolTips = toolTips.filter((x) => !sameFact(x.tip, t.tip));
|
|
461
|
+
toolTips.push({ ...t, at: now });
|
|
462
|
+
added.toolTips.push(t.tip);
|
|
463
|
+
}
|
|
464
|
+
const pb = r && r.playbook ? cleanPlaybook(r.playbook) : null;
|
|
465
|
+
if (pb) {
|
|
466
|
+
const old = playbooks.find((x) => playbookKey(x) === playbookKey(pb));
|
|
467
|
+
const merged = old
|
|
468
|
+
? { ...pb, when: pb.when || old.when, params: pb.params || old.params, precautions: uniqueStrings([...old.precautions, ...pb.precautions], 6), uses: old.uses + 1, at: now }
|
|
469
|
+
: { ...pb, uses: 1, at: now };
|
|
470
|
+
playbooks = playbooks.filter((x) => x !== old);
|
|
471
|
+
playbooks.push(merged); // most recently used last, so the oldest unused one goes first
|
|
472
|
+
added.playbook = playbookName(merged);
|
|
473
|
+
added.updated = !!old;
|
|
474
|
+
}
|
|
475
|
+
// Open threads: resolved ones go (by number or by text), a new one is added.
|
|
476
|
+
const resolved = Array.isArray(r && r.resolved) ? r.resolved : [];
|
|
477
|
+
if (resolved.length) {
|
|
478
|
+
const before = openThreads;
|
|
479
|
+
openThreads = openThreads.filter((t, i) => !resolved.some((x) => (typeof x === 'number' ? x === i + 1 : (/^\d+$/.test(String(x).trim()) ? Number(x) === i + 1 : sameFact(t.text, x)))));
|
|
480
|
+
added.resolved = before.filter((t) => !openThreads.includes(t)).map((t) => t.text);
|
|
481
|
+
}
|
|
482
|
+
const unfinished = r && typeof r.unfinished === 'string' && noSecret(r.unfinished) ? clip(r.unfinished, 200) : '';
|
|
483
|
+
if (unfinished && !/^(none|nothing|n\/a|no)\.?$/i.test(unfinished)) {
|
|
484
|
+
openThreads = openThreads.filter((t) => !sameFact(t.text, unfinished));
|
|
485
|
+
openThreads.push({ text: unfinished, at: now });
|
|
486
|
+
added.unfinished = unfinished;
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
experience: {
|
|
490
|
+
lessons: lessons.slice(-MAX_LESSONS), playbooks: playbooks.slice(-MAX_PLAYBOOKS),
|
|
491
|
+
toolTips: toolTips.slice(-MAX_TOOL_TIPS), openThreads: openThreads.slice(-MAX_OPEN_THREADS),
|
|
492
|
+
},
|
|
493
|
+
added,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Merge a reflection into the stored bot (re-read first, so a parallel learn is never lost). */
|
|
498
|
+
function addExperience(botId, reflection) {
|
|
499
|
+
const bot = getBot(botId);
|
|
500
|
+
if (!bot) return { bot: null, added: null };
|
|
501
|
+
const { experience, added } = mergeExperience(bot, reflection);
|
|
502
|
+
return { bot: save({ ...bot, ...experience, updatedAt: Date.now() }), added };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const EXPERIENCE_KINDS = ['lessons', 'playbooks', 'toolTips', 'openThreads'];
|
|
506
|
+
|
|
507
|
+
/** Forget one experience item (kind + index), or everything with kind 'all'. */
|
|
508
|
+
function forgetExperience(botId, kind, index) {
|
|
509
|
+
const bot = getBot(botId);
|
|
510
|
+
if (!bot) throw new Error('That bot is gone.');
|
|
511
|
+
if (kind === 'all') return save({ ...bot, lessons: [], playbooks: [], toolTips: [], openThreads: [], updatedAt: Date.now() });
|
|
512
|
+
if (!EXPERIENCE_KINDS.includes(kind)) throw new Error('Unknown kind of experience.');
|
|
513
|
+
const list = bot[kind].slice();
|
|
514
|
+
if (index >= 0 && index < list.length) list.splice(index, 1);
|
|
515
|
+
return save({ ...bot, [kind]: list, updatedAt: Date.now() });
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const STOP = new Set('the and for with that this from what when where which your you our are was were can could would should will have has had not but all any how who why into onto about please just some them they then than there these those its it him her his she use using want need make get got give show tell check look find out new one two'.split(' '));
|
|
519
|
+
|
|
520
|
+
/** Words that carry meaning, lightly stemmed ("emails" and "email" match). */
|
|
521
|
+
function terms(text) {
|
|
522
|
+
return [...new Set(String(text || '').toLowerCase().replace(/[^a-z0-9 ]+/g, ' ').split(/\s+/)
|
|
523
|
+
.filter((w) => w.length >= 3 && !STOP.has(w))
|
|
524
|
+
.map((w) => (w.length > 4 ? w.replace(/(ing|es|s)$/, '') : w)))];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/** The playbooks that fit a request best (keyword overlap), at most `n`, best first. */
|
|
528
|
+
function rankPlaybooks(playbooks, request, n = PLAYBOOKS_IN_FULL) {
|
|
529
|
+
const want = terms(request);
|
|
530
|
+
if (!want.length) return [];
|
|
531
|
+
return (playbooks || []).map((p, i) => {
|
|
532
|
+
const head = terms(`${p.app} ${p.task}`);
|
|
533
|
+
const body = terms(`${p.when} ${(p.steps || []).join(' ')}`);
|
|
534
|
+
let score = 0;
|
|
535
|
+
for (const w of want) score += head.includes(w) ? 2 : body.includes(w) ? 0.5 : 0;
|
|
536
|
+
return { p, i, score };
|
|
537
|
+
}).filter((x) => x.score >= 2 || (x.score >= 1 && want.length <= 2))
|
|
538
|
+
.sort((a, b) => b.score - a.score || b.p.at - a.p.at)
|
|
539
|
+
.slice(0, n).map((x) => x.p);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/** One line per tool call: name, short args, ok or the error. */
|
|
543
|
+
function trajectoryLines(steps) {
|
|
544
|
+
return (Array.isArray(steps) ? steps : []).filter((s) => s && s.name).slice(-30).map((s, i) => {
|
|
545
|
+
let args = '';
|
|
546
|
+
if (s.args && typeof s.args === 'object') {
|
|
547
|
+
const short = {};
|
|
548
|
+
for (const [k, v] of Object.entries(s.args)) {
|
|
549
|
+
if (/content|patch|body|text|old|new/i.test(k) && typeof v === 'string' && v.length > 60) short[k] = `(${v.length} chars)`;
|
|
550
|
+
else short[k] = typeof v === 'string' ? clip(v, 80) : v;
|
|
551
|
+
}
|
|
552
|
+
args = clip(JSON.stringify(short), 200);
|
|
553
|
+
} else if (s.label) args = clip(s.label, 140);
|
|
554
|
+
const result = s.ok ? 'ok' : `error${s.error ? `: ${clip(s.error, 140)}` : ''}`;
|
|
555
|
+
return `${i + 1}. ${s.name} ${args} -> ${result}`;
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function reflectPrompt(bot, { request, steps, reply }) {
|
|
560
|
+
const index = (bot.playbooks || []).map((p) => `- ${playbookName(p)}`).join('\n') || '(none yet)';
|
|
561
|
+
const lessons = (bot.lessons || []).map((l) => `- When ${l.situation}: ${l.strategy}`).join('\n') || '(none yet)';
|
|
562
|
+
const threads = (bot.openThreads || []).map((t, i) => `${i + 1}. ${t.text}`).join('\n') || '(none)';
|
|
563
|
+
return `You are the reflection step of an assistant called ${bot.name}${bot.specialty ? ` (${bot.specialty})` : ''}. It just finished a piece of work with its tools. Look at what it did and decide what it should learn for next time, so it gets better on the job.
|
|
564
|
+
|
|
565
|
+
The user's request:
|
|
566
|
+
${clip(request, 2000) || '(not given)'}
|
|
567
|
+
|
|
568
|
+
The tool calls, in order:
|
|
569
|
+
${trajectoryLines(steps).join('\n') || '(none)'}
|
|
570
|
+
|
|
571
|
+
${bot.name}'s final reply:
|
|
572
|
+
${clip(reply, 2000) || '(no reply)'}
|
|
573
|
+
|
|
574
|
+
Playbooks it already has (reuse the exact same app and task names to update one):
|
|
575
|
+
${index}
|
|
576
|
+
|
|
577
|
+
Lessons it already has:
|
|
578
|
+
${lessons}
|
|
579
|
+
|
|
580
|
+
Open threads it left unfinished before (numbered):
|
|
581
|
+
${threads}
|
|
582
|
+
|
|
583
|
+
Judge the outcome from the tool results, not from what the reply claims. Then respond with ONLY a JSON object:
|
|
584
|
+
{
|
|
585
|
+
"outcome": "success" or "partial" or "failed",
|
|
586
|
+
"playbook": null, or ONLY when it succeeded and the procedure is reusable: {"app": "the app or area, e.g. Gmail, Files, Web, Shell", "task": "short task name, e.g. find important emails", "when": "one line: when to use it", "steps": ["each key step, naming the real tool used, e.g. gmail_search with q=is:important newer_than:2d"], "precautions": ["what to watch out for"], "params": "parameters or values that worked, or empty"},
|
|
587
|
+
"lessons": [{"situation": "a short kind of situation", "strategy": "what works there, or what to avoid"}] (at most 2, only new and general ones),
|
|
588
|
+
"toolTips": ["tool_name: how it behaved, e.g. a query or argument that worked or failed"] (at most 2),
|
|
589
|
+
"unfinished": "one line about anything the user asked for that is still not done, or empty",
|
|
590
|
+
"resolved": [numbers of the open threads above that this work finished]
|
|
591
|
+
}
|
|
592
|
+
Never include passwords, keys, tokens or personal secrets. Keep every line short. Never use the long dash character.`;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* MUSE "Reflect + Memorize": after a run that used tools, one cheap model
|
|
597
|
+
* call turns the trajectory into experience on the bot. chatFn has the
|
|
598
|
+
* ai.js chatJson shape. Never throws.
|
|
599
|
+
* @param {object} bot
|
|
600
|
+
* @param {{request:string, steps:Array<{name:string,args?:object,label?:string,ok:boolean,error?:string}>, reply:string}} run
|
|
601
|
+
* @returns {Promise<{outcome?:string, added:object|null, skipped?:string}>}
|
|
602
|
+
*/
|
|
603
|
+
async function reflectOnRun(bot, run, chatFn) {
|
|
604
|
+
try {
|
|
605
|
+
if (!bot || !bot.id || typeof chatFn !== 'function') return { added: null, skipped: 'no bot or model' };
|
|
606
|
+
const steps = (run && Array.isArray(run.steps) ? run.steps : []).filter((s) => s && s.name);
|
|
607
|
+
if (!steps.length) return { added: null, skipped: 'no tools used' };
|
|
608
|
+
const fresh = getBot(bot.id) || normalizeBot(bot, bot);
|
|
609
|
+
const r = await chatFn([{ role: 'user', content: reflectPrompt(fresh, { ...run, steps }) }]);
|
|
610
|
+
if (!r || !r.success || !r.json || typeof r.json !== 'object') return { added: null, skipped: 'no answer' };
|
|
611
|
+
const j = r.json;
|
|
612
|
+
const outcome = ['success', 'partial', 'failed'].includes(j.outcome) ? j.outcome : 'partial';
|
|
613
|
+
const reflection = {
|
|
614
|
+
playbook: outcome === 'success' ? j.playbook : null,
|
|
615
|
+
lessons: (Array.isArray(j.lessons) ? j.lessons : []).slice(0, 2),
|
|
616
|
+
toolTips: (Array.isArray(j.toolTips) ? j.toolTips : []).slice(0, 2),
|
|
617
|
+
unfinished: j.unfinished,
|
|
618
|
+
resolved: j.resolved,
|
|
619
|
+
};
|
|
620
|
+
const { added } = addExperience(bot.id, reflection);
|
|
621
|
+
return { outcome, added };
|
|
622
|
+
} catch {
|
|
623
|
+
return { added: null, skipped: 'failed' };
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
356
627
|
// ─── Prompts ────────────────────────────────────────────────────────────────
|
|
357
628
|
|
|
358
629
|
function approvalLine(bot) {
|
|
@@ -374,6 +645,45 @@ The one task, with every bit of context they need (they see nothing else).
|
|
|
374
645
|
</task>
|
|
375
646
|
</codeply:ask_bot>`;
|
|
376
647
|
|
|
648
|
+
const VERIFY_RULES = `BEFORE YOU SAY IT IS DONE
|
|
649
|
+
- Base every claim on what your tools actually returned in this run.
|
|
650
|
+
- Check the deliverable exists (the file is written, the message is sent, the command passed) before you say so.
|
|
651
|
+
- Never invent results, numbers, names or links. If something failed or you could not check it, say so plainly.`;
|
|
652
|
+
|
|
653
|
+
const shortDate = (at) => new Date(Number(at) || Date.now()).toISOString().slice(0, 10);
|
|
654
|
+
|
|
655
|
+
function playbookText(p) {
|
|
656
|
+
const lines = [`${playbookName(p)}`];
|
|
657
|
+
if (p.when) lines.push(`When to use: ${p.when}`);
|
|
658
|
+
lines.push('Steps:', ...(p.steps || []).map((s, i) => `${i + 1}. ${s}`));
|
|
659
|
+
if ((p.precautions || []).length) lines.push('Precautions:', ...p.precautions.map((s) => `- ${s}`));
|
|
660
|
+
if (p.params) lines.push(`What worked: ${p.params}`);
|
|
661
|
+
return lines.join('\n');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** The experience sections of a bot's prompt; `request` picks the playbooks shown in full. */
|
|
665
|
+
function experienceParts(bot, request) {
|
|
666
|
+
const parts = [];
|
|
667
|
+
const lessons = bot.lessons || [];
|
|
668
|
+
if (lessons.length) {
|
|
669
|
+
parts.push(`WHAT YOU HAVE LEARNED FROM PAST WORK\nStrategies from your own earlier work. Use them when the situation fits:\n${lessons.map((l) => `- When ${l.situation}: ${l.strategy}`).join('\n')}`);
|
|
670
|
+
}
|
|
671
|
+
const tips = bot.toolTips || [];
|
|
672
|
+
if (tips.length) parts.push(`TOOL TIPS\nHow your tools behaved before:\n${tips.map((t) => `- ${t.tip}`).join('\n')}`);
|
|
673
|
+
const playbooks = bot.playbooks || [];
|
|
674
|
+
if (playbooks.length) {
|
|
675
|
+
const top = rankPlaybooks(playbooks, request);
|
|
676
|
+
const index = playbooks.slice().reverse().map((p) => `- ${playbookName(p)}${p.when ? `: ${clip(p.when, 90)}` : ''}${top.includes(p) ? ' (in full below)' : ''}`);
|
|
677
|
+
parts.push(`YOUR PLAYBOOKS\nProcedures that worked for you before (app / task). Follow the matching one, and adapt it if things changed:\n${index.join('\n')}`);
|
|
678
|
+
if (top.length) parts.push(`PLAYBOOKS FOR THIS REQUEST\n${top.map(playbookText).join('\n\n')}`);
|
|
679
|
+
}
|
|
680
|
+
const threads = bot.openThreads || [];
|
|
681
|
+
if (threads.length) {
|
|
682
|
+
parts.push(`OPEN THREADS\nThings you left unfinished before. Bring one up when it fits, and finish it when the user wants:\n${threads.map((t) => `- (${shortDate(t.at)}) ${t.text}`).join('\n')}`);
|
|
683
|
+
}
|
|
684
|
+
return parts;
|
|
685
|
+
}
|
|
686
|
+
|
|
377
687
|
/**
|
|
378
688
|
* The system prompt addition for one bot.
|
|
379
689
|
* @param {object} bot
|
|
@@ -382,6 +692,7 @@ The one task, with every bit of context they need (they see nothing else).
|
|
|
382
692
|
* @param {boolean} [o.canDelegate] ask_bot is available in this run
|
|
383
693
|
* @param {string} [o.askedBy] set for a delegated sub-run: who asked
|
|
384
694
|
* @param {boolean} [o.native] native function calling (no tag example)
|
|
695
|
+
* @param {string} [o.request] the user's message: picks the playbooks shown in full
|
|
385
696
|
*/
|
|
386
697
|
function buildBotPrompt(bot, o = {}) {
|
|
387
698
|
if (!bot) return '';
|
|
@@ -398,6 +709,8 @@ function buildBotPrompt(bot, o = {}) {
|
|
|
398
709
|
if (memory.length) {
|
|
399
710
|
parts.push(`WHAT YOU HAVE LEARNED ABOUT THIS USER\nFollow these unless the user says otherwise now:\n${memory.map((m) => `- ${m.fact}`).join('\n')}`);
|
|
400
711
|
}
|
|
712
|
+
parts.push(...experienceParts(bot, o.request));
|
|
713
|
+
parts.push(VERIFY_RULES);
|
|
401
714
|
if (o.askedBy) {
|
|
402
715
|
parts.push(`DELEGATED TASK\n${o.askedBy} asked you to do one task. You only see that task, not the rest of the conversation. Do it fully with your tools, then reply in this shape:\nSummary: <one sentence with the outcome>\n<the details: findings, what you changed, anything ${o.askedBy} must know or check>`);
|
|
403
716
|
}
|
|
@@ -522,7 +835,7 @@ async function delegate(o) {
|
|
|
522
835
|
try {
|
|
523
836
|
reply = await lock.run(o.token, () => o.runBot(target, {
|
|
524
837
|
task, depth: depth + 1, chain: [...chain, ...(o.caller ? [o.caller.id] : []), target.id],
|
|
525
|
-
prompt: buildBotPrompt(target, { team, askedBy: callerName, canDelegate: depth + 1 < MAX_DEPTH }),
|
|
838
|
+
prompt: buildBotPrompt(target, { team, askedBy: callerName, canDelegate: depth + 1 < MAX_DEPTH, request: task }),
|
|
526
839
|
}), o.signal);
|
|
527
840
|
} catch (e) {
|
|
528
841
|
const output = `${target.name} could not finish: ${e.message}`;
|
|
@@ -576,14 +889,113 @@ function fromDescription(json, description) {
|
|
|
576
889
|
// Irreversible actions always stay behind approval, whatever the model said.
|
|
577
890
|
for (const k of ['send', 'publish', 'databases']) if (!draft.approval.includes(k)) draft.approval.push(k);
|
|
578
891
|
delete draft.id; delete draft.memory; delete draft.createdAt; delete draft.updatedAt;
|
|
892
|
+
delete draft.lessons; delete draft.playbooks; delete draft.toolTips; delete draft.openThreads;
|
|
579
893
|
return draft;
|
|
580
894
|
}
|
|
581
895
|
|
|
896
|
+
// ─── Voice calls ────────────────────────────────────────────────────────────
|
|
897
|
+
// A call turn is a real agent run: the bot keeps its tools (Gmail, files, the
|
|
898
|
+
// web, commands) and its approval boundary, and only the way it answers
|
|
899
|
+
// changes, so "check my email" on a call really checks it.
|
|
900
|
+
|
|
901
|
+
const VOICE_RULES = `LIVE VOICE CALL
|
|
902
|
+
You are on a live voice call with the user, talking out loud. Your final reply is spoken by a voice engine.
|
|
903
|
+
- You still have all your tools. When the user asks for real work (check their email, look something up, read or change a file), do it now with your tools, then tell them the result.
|
|
904
|
+
- Speak the result in one to three short sentences, the way people talk. Summarize: for emails say who it is from and what it is about, never read out long text, links or ids.
|
|
905
|
+
- No markdown, no lists, no headings, no emojis, no code, no links, no long dash in what you say.
|
|
906
|
+
- Ask one short question back when it helps.
|
|
907
|
+
- Anything that needs the user's OK (sending, changing files, running commands) is asked on their screen; if they say no, tell them plainly.`;
|
|
908
|
+
|
|
909
|
+
const VOICE_RULES_NO_TOOLS = `LIVE VOICE CALL
|
|
910
|
+
You are on a live voice call with the user, talking out loud. Everything you write is spoken by a voice engine.
|
|
911
|
+
- Reply in one to three short spoken sentences. Plain words, the way people talk.
|
|
912
|
+
- No markdown, no lists, no headings, no emojis, no code, no links, no long dash.
|
|
913
|
+
- Ask one short question back when it helps.
|
|
914
|
+
- Right now you cannot use your tools (email, files, the web), because the user's PC is offline. If they ask for that kind of work, say so in one sentence and offer to do it once their PC is on.`;
|
|
915
|
+
|
|
916
|
+
/** Whatever the model sent, make it sayable: no markdown, no long dashes. */
|
|
917
|
+
function spoken(s) {
|
|
918
|
+
const dash = String.fromCharCode(0x2014);
|
|
919
|
+
return String(s || '')
|
|
920
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, '')
|
|
921
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
922
|
+
.replace(/\[(.*?)\]\((.*?)\)/g, '$1')
|
|
923
|
+
.replace(/https?:\/\/\S+/g, '')
|
|
924
|
+
.replace(/[*_#`>]+/g, '')
|
|
925
|
+
.split(dash).join(', ')
|
|
926
|
+
.replace(/\s+/g, ' ')
|
|
927
|
+
.trim();
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** The call so far as chat turns: the last thing the user said is the new message. */
|
|
931
|
+
function callTurns(turns) {
|
|
932
|
+
const list = (Array.isArray(turns) ? turns : []).filter((t) => t && t.text).slice(-16)
|
|
933
|
+
.map((t) => ({ role: t.who === 'bot' ? 'assistant' : 'user', content: String(t.text).slice(0, 1200) }));
|
|
934
|
+
let lastUser = -1;
|
|
935
|
+
list.forEach((t, i) => { if (t.role === 'user') lastUser = i; });
|
|
936
|
+
if (lastUser < 0) return { history: [], userMessage: '' };
|
|
937
|
+
const history = [];
|
|
938
|
+
for (const t of list.slice(0, lastUser)) {
|
|
939
|
+
const prev = history[history.length - 1];
|
|
940
|
+
if (prev && prev.role === t.role) prev.content += `\n${t.content}`; else history.push({ ...t });
|
|
941
|
+
}
|
|
942
|
+
while (history.length && history[0].role !== 'user') history.shift();
|
|
943
|
+
return { history, userMessage: list.slice(lastUser).filter((t) => t.role === 'user').map((t) => t.content).join('\n') };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* One spoken reply on a call, with tools. `runAgent` is the engine's agent
|
|
948
|
+
* loop; `onStep({ name, label, done, ok })` reports tool use for the call
|
|
949
|
+
* screen ("Checking Gmail..."). Returns { text } ready to speak, plus
|
|
950
|
+
* { request, reply, steps } for reflectOnRun.
|
|
951
|
+
*/
|
|
952
|
+
async function voiceTurn({ bot, team, turns, recentChat, runAgent, route, cwd, signal, approve, onStep, maxSteps = 16 }) {
|
|
953
|
+
const { history, userMessage } = callTurns(turns);
|
|
954
|
+
if (!userMessage) return { text: '' };
|
|
955
|
+
const extra = recentChat ? `\n\nRECENT CHAT BEFORE THIS CALL\n${recentChat}` : '';
|
|
956
|
+
const run = runAgent({
|
|
957
|
+
userMessage, history, mode: 'Build', cwd, signal, route, maxSteps, approve,
|
|
958
|
+
botPrompt: () => `${buildBotPrompt(bot, { team, request: userMessage })}\n\n${VOICE_RULES}${extra}`,
|
|
959
|
+
});
|
|
960
|
+
let reply = '';
|
|
961
|
+
const steps = []; // the trajectory, for reflectOnRun after the call turn
|
|
962
|
+
for await (const ev of run) {
|
|
963
|
+
if (ev.type === 'text' && !ev.interim) reply += (reply ? ' ' : '') + ev.text;
|
|
964
|
+
else if (ev.type === 'tool_start' && onStep) onStep({ name: ev.name, args: ev.args || {} });
|
|
965
|
+
else if (ev.type === 'tool_end') {
|
|
966
|
+
steps.push(runStep(ev));
|
|
967
|
+
if (onStep) onStep({ name: ev.name, args: ev.args || {}, done: true, ok: !!ev.ok });
|
|
968
|
+
}
|
|
969
|
+
else if (ev.type === 'error') throw new Error(ev.error || 'The run failed.');
|
|
970
|
+
else if (ev.type === 'aborted') throw new Error('Stopped.');
|
|
971
|
+
else if (ev.type === 'done') break;
|
|
972
|
+
}
|
|
973
|
+
if (signal && signal.aborted) throw new Error('Stopped.'); // hung up or talked over: nothing to say
|
|
974
|
+
return { text: spoken(reply), request: userMessage, reply, steps };
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** One agent tool_end event as a trajectory step for reflectOnRun. */
|
|
978
|
+
function runStep(ev) {
|
|
979
|
+
return { name: ev.name, args: ev.args || {}, ok: !!ev.ok, error: ev.ok ? undefined : clip(ev.summary || ev.error || '', 200) };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/** What the call screen says while a tool runs. */
|
|
983
|
+
const CALL_STEP = {
|
|
984
|
+
gmail_search: 'Checking your email', gmail_send: 'Sending the email', web_search: 'Searching the web', web_fetch: 'Reading a page',
|
|
985
|
+
read_file: 'Reading a file', write_file: 'Writing a file', edit_file: 'Editing a file', apply_patch: 'Editing files', run: 'Running a command',
|
|
986
|
+
search: 'Searching your files', list_dir: 'Looking through files', slack_post_message: 'Posting to Slack', browser_check: 'Checking the page',
|
|
987
|
+
ask_bot: 'Asking a teammate', todo: 'Planning',
|
|
988
|
+
};
|
|
989
|
+
const callStepLabel = (name) => CALL_STEP[name] || 'Working on it';
|
|
990
|
+
|
|
582
991
|
module.exports = {
|
|
992
|
+
VOICE_RULES, VOICE_RULES_NO_TOOLS, spoken, callTurns, voiceTurn, callStepLabel,
|
|
583
993
|
MAX_MEMORY, MAX_DEPTH, ROLES, TONES, APPROVALS, AVATAR_KEYS, TEMPLATES,
|
|
584
994
|
botsDir, setBotsDir, normalizeBot, normalizeAvatar,
|
|
585
995
|
listBots, getBot, findBot, createBot, createFromTemplate, updateBot, removeBot,
|
|
586
996
|
mergeMemory, addMemory, forget, clearMemory, learnFromTurn,
|
|
997
|
+
MAX_LESSONS, MAX_PLAYBOOKS, MAX_TOOL_TIPS, MAX_OPEN_THREADS,
|
|
998
|
+
mergeExperience, addExperience, forgetExperience, rankPlaybooks, reflectOnRun, runStep,
|
|
587
999
|
buildBotPrompt, teamPrompt, approvalCategory, canSkipApproval,
|
|
588
1000
|
createLock, globalLock, parseResult, delegate, botCard,
|
|
589
1001
|
describePrompt, fromDescription,
|
package/lib/cloud.mjs
CHANGED
|
@@ -43,6 +43,7 @@ export const checkName = (id) => `craft ${id}`;
|
|
|
43
43
|
const MAX_FILE_BYTES = 50 * 1024 * 1024;
|
|
44
44
|
const MAX_SCAN_BYTES = 2 * 1024 * 1024;
|
|
45
45
|
const MAX_PROMPT = 20000;
|
|
46
|
+
const MAX_BOT_PROMPT = 30000;
|
|
46
47
|
const MAX_OUTPUT = 60000;
|
|
47
48
|
const HISTORY_TURNS = 40;
|
|
48
49
|
|
|
@@ -522,7 +523,7 @@ async function dispatch(api, repo, base, task, retryMs) {
|
|
|
522
523
|
* queue at once; otherwise a new runner is dispatched. Mirror projects push
|
|
523
524
|
* the latest snapshot first.
|
|
524
525
|
*/
|
|
525
|
-
export async function startCloudRun({ cwd, token, prompt, mode = 'Build', sessionId = '', model, home = defaultHome(), apiUrl, serverUrl, fetchImpl, remoteUrl, retryMs = 3000, startedAt }) {
|
|
526
|
+
export async function startCloudRun({ cwd, token, prompt, mode = 'Build', sessionId = '', bot = null, kind = 'code', model, home = defaultHome(), apiUrl, serverUrl, fetchImpl, remoteUrl, retryMs = 3000, startedAt }) {
|
|
526
527
|
const p = getProject(cwd, home);
|
|
527
528
|
if (!p || !p.repo) throw new Error('Set up cloud runs for this project first.');
|
|
528
529
|
prompt = String(prompt || '').trim();
|
|
@@ -547,11 +548,21 @@ export async function startCloudRun({ cwd, token, prompt, mode = 'Build', sessio
|
|
|
547
548
|
baseSha = (await pushSnapshot({ cwd, token, home, serverUrl, remoteUrl })).sha;
|
|
548
549
|
}
|
|
549
550
|
const task = { id: newTaskId(), prompt, mode, sessionId: String(sessionId || ''), baseSha, repo: p.repo, status: 'starting', startedAt: startedAt || Date.now() };
|
|
551
|
+
const b = cleanBot(bot);
|
|
552
|
+
const extra = { ...(b ? { bot: b } : {}), ...(cleanKind(kind) === 'task' ? { kind: 'task' } : {}) };
|
|
553
|
+
if (b) task.bot = { name: b.name };
|
|
554
|
+
if (extra.kind) task.kind = 'task';
|
|
555
|
+
const files = branchFiles(api, p.repo);
|
|
550
556
|
const live = await liveRunner(api, p.repo, task.sessionId);
|
|
551
557
|
if (live) {
|
|
552
|
-
await
|
|
558
|
+
await files.write(`${SESSION_PATHS.queue(task.sessionId)}/${Date.now()}-${task.id}.json`, { id: task.id, prompt, mode, ...extra }, `Craft queue ${task.id}`);
|
|
553
559
|
Object.assign(task, { queued: true, enqueuedAt: Date.now() });
|
|
554
560
|
} else {
|
|
561
|
+
// A bot's prompt is too big for a workflow input: the runner reads it from craft-sessions.
|
|
562
|
+
if (Object.keys(extra).length) {
|
|
563
|
+
if (baseSha) await files.ensure(baseSha).catch(() => {});
|
|
564
|
+
if (!(await files.write(SESSION_PATHS.request(task.id), extra, `Craft request ${task.id}`))) throw new Error('Could not hand the bot to the cloud. Try again.');
|
|
565
|
+
}
|
|
555
566
|
await dispatch(api, p.repo, base, task, retryMs);
|
|
556
567
|
}
|
|
557
568
|
saveTask(cwd, home, task);
|
|
@@ -834,8 +845,23 @@ export const SESSION_PATHS = {
|
|
|
834
845
|
live: (sid) => `live/${sid}.json`,
|
|
835
846
|
queue: (sid) => `queue/${sid}`,
|
|
836
847
|
shot: (id, n) => `shots/${id}/${n}.png`,
|
|
848
|
+
// What does not fit in the workflow's inputs (a bot's prompt), written before the dispatch.
|
|
849
|
+
request: (id) => `requests/${id}.json`,
|
|
837
850
|
};
|
|
838
851
|
|
|
852
|
+
/**
|
|
853
|
+
* A task's optional bot: { name, prompt }, the full prompt the PC built
|
|
854
|
+
* (bots.js buildBotPrompt). Anything else, or an empty prompt, is no bot.
|
|
855
|
+
*/
|
|
856
|
+
export function cleanBot(bot) {
|
|
857
|
+
if (!bot || typeof bot !== 'object') return null;
|
|
858
|
+
const prompt = String(bot.prompt || '').trim().slice(0, MAX_BOT_PROMPT);
|
|
859
|
+
if (!prompt) return null;
|
|
860
|
+
return { name: String(bot.name || 'Bot').trim().slice(0, 80) || 'Bot', prompt };
|
|
861
|
+
}
|
|
862
|
+
/** 'task' is a job for a bot (not about the repo): its final reply is what the user hears. */
|
|
863
|
+
const cleanKind = (k) => (k === 'task' ? 'task' : 'code');
|
|
864
|
+
|
|
839
865
|
/** Small read / write / delete / list helpers for one branch, with retries for concurrent writes. */
|
|
840
866
|
export function branchFiles(api, repo, branch = SESSIONS_BRANCH) {
|
|
841
867
|
const enc = (p) => p.split('/').map(encodeURIComponent).join('/');
|
|
@@ -929,6 +955,12 @@ export async function runCloudRunner({ env = process.env, cwd = process.cwd(), t
|
|
|
929
955
|
const heartbeat = (busy, until) => files.write(SESSION_PATHS.live(sessionId), { runId: env.GITHUB_RUN_ID || null, busy, at: Date.now(), until }, `Craft live ${sessionId}`).catch(() => {});
|
|
930
956
|
|
|
931
957
|
let task = { id: firstId, prompt: String(env.CRAFT_PROMPT || ''), mode: env.CRAFT_MODE, sessionId, dispatched: true };
|
|
958
|
+
// A bot (and the task kind) comes next to the dispatch, on craft-sessions.
|
|
959
|
+
const req = await files.read(SESSION_PATHS.request(firstId)).catch(() => null);
|
|
960
|
+
if (req && req.json) {
|
|
961
|
+
task = { ...task, bot: req.json.bot, kind: req.json.kind };
|
|
962
|
+
await files.remove(SESSION_PATHS.request(firstId), req.sha).catch(() => {});
|
|
963
|
+
}
|
|
932
964
|
const results = [];
|
|
933
965
|
try {
|
|
934
966
|
while (task) {
|
|
@@ -971,6 +1003,8 @@ async function runOneTask({ task, env, cwd, api, files, repo, token, serverUrl,
|
|
|
971
1003
|
const mode = ['Build', 'Plan', 'Ask'].includes(task.mode) ? task.mode : 'Build';
|
|
972
1004
|
const prompt = String(task.prompt || '').slice(0, MAX_PROMPT);
|
|
973
1005
|
const sessionId = task.sessionId || '';
|
|
1006
|
+
const bot = cleanBot(task.bot);
|
|
1007
|
+
const kind = cleanKind(task.kind);
|
|
974
1008
|
const startedAt = Date.now();
|
|
975
1009
|
const events = [];
|
|
976
1010
|
const lines = [];
|
|
@@ -981,7 +1015,7 @@ async function runOneTask({ task, env, cwd, api, files, repo, token, serverUrl,
|
|
|
981
1015
|
output: { title: 'Starting', summary: 'Starting...' },
|
|
982
1016
|
}).catch(() => ({ json: null }))).json : null;
|
|
983
1017
|
|
|
984
|
-
const statusDoc = (status, extra = {}) => ({ id: taskId, status, mode, prompt: prompt.slice(0, 4000), sessionId, startedAt, updatedAt: Date.now(), runId: env.GITHUB_RUN_ID || null, ...extra });
|
|
1018
|
+
const statusDoc = (status, extra = {}) => ({ id: taskId, status, mode, prompt: prompt.slice(0, 4000), sessionId, ...(kind === 'task' ? { kind } : {}), ...(bot ? { bot: { name: bot.name } } : {}), startedAt, updatedAt: Date.now(), runId: env.GITHUB_RUN_ID || null, ...extra });
|
|
985
1019
|
const finish = async (ok, title, summary, result) => {
|
|
986
1020
|
const baseLen = JSON.stringify(result).length;
|
|
987
1021
|
const packed = packEvents(events, Math.max(4000, MAX_OUTPUT - baseLen - 200));
|
|
@@ -1031,8 +1065,10 @@ async function runOneTask({ task, env, cwd, api, files, repo, token, serverUrl,
|
|
|
1031
1065
|
await pushProgress(hist.messages.length ? `Continuing the chat (${hist.messages.length / 2} earlier turns).` : 'Working on it.', true);
|
|
1032
1066
|
const approve = ciApprove(cwd);
|
|
1033
1067
|
let answer = ''; let steps = 0; let failure = '';
|
|
1034
|
-
const userMessage =
|
|
1035
|
-
|
|
1068
|
+
const userMessage = kind === 'task'
|
|
1069
|
+
? `${prompt}\n\n(You are running unattended in Codeply Cloud on a Linux machine, not on the user's PC: you cannot reach their local files or desktop apps. Use the shell, the web and this repository. Nobody can answer questions mid-run, so make sensible choices and say what you assumed. Your final reply is read out to the user: say plainly what you did or found, in a few short sentences, no markdown. Do not commit or push; Craft does that when you finish.)`
|
|
1070
|
+
: `${prompt}\n\n(You are running unattended in Craft Cloud, in a fresh copy of the repository on a Linux machine. Nobody can answer questions mid-run, so make sensible choices and say what you assumed. You can install dependencies, run the app and tests, and check pages with browser_check. Do not commit or push; Craft does that when you finish.)`;
|
|
1071
|
+
for await (const ev of runAgentImpl({ userMessage, history: hist.messages.slice(-HISTORY_TURNS), mode, cwd, approve, browser: agentBrowser, signal: new AbortController().signal, route, maxSteps, ...(bot ? { botPrompt: bot.prompt } : {}) })) {
|
|
1036
1072
|
const rec = recordEvent(ev);
|
|
1037
1073
|
if (rec && rec.t === 'tool' && ev.name === 'browser_check' && pendingShot) { rec.screenshot = pendingShot; pendingShot = null; }
|
|
1038
1074
|
// The final answer arrives as the reply; everything else is part of the chat's steps.
|
|
@@ -1046,6 +1082,8 @@ async function runOneTask({ task, env, cwd, api, files, repo, token, serverUrl,
|
|
|
1046
1082
|
if (pending) await pending;
|
|
1047
1083
|
if (failure && !answer) throw new Error(failure);
|
|
1048
1084
|
|
|
1085
|
+
// The bot's own last words, before a plan file replaces the answer: what the phone speaks.
|
|
1086
|
+
const reply = answer;
|
|
1049
1087
|
if (mode === 'Plan') {
|
|
1050
1088
|
const dir = path.join(cwd, '.codeply', 'plans');
|
|
1051
1089
|
const plans = fs.existsSync(dir) ? fs.readdirSync(dir).filter((f) => f.endsWith('.md')).map((f) => ({ f, t: fs.statSync(path.join(dir, f)).mtimeMs })).sort((a, b) => b.t - a.t) : [];
|
|
@@ -1088,7 +1126,8 @@ async function runOneTask({ task, env, cwd, api, files, repo, token, serverUrl,
|
|
|
1088
1126
|
|
|
1089
1127
|
await saveHistory(files, sessionId, [...hist.messages, { role: 'user', content: prompt }, { role: 'assistant', content: answer || '(no reply)' }].slice(-HISTORY_TURNS * 2))
|
|
1090
1128
|
.catch((e) => log(`Could not save the chat: ${e.message}`));
|
|
1091
|
-
const result = { mode, prompt: prompt.slice(0, 4000), sessionId, answer: answer.slice(0, 20000), branch, sha, files: changed, stats: stats.slice(0, 100), merged, steps, baseSha: taskBase
|
|
1129
|
+
const result = { mode, prompt: prompt.slice(0, 4000), sessionId, answer: answer.slice(0, 20000), branch, sha, files: changed, stats: stats.slice(0, 100), merged, steps, baseSha: taskBase,
|
|
1130
|
+
...(bot || kind === 'task' ? { reply: reply.slice(0, 20000) } : {}) };
|
|
1092
1131
|
const title = changed.length ? `Changed ${changed.length} file${changed.length === 1 ? '' : 's'}` : mode === 'Build' ? 'No files changed' : 'Answered';
|
|
1093
1132
|
return finish(true, title, answer || title, result);
|
|
1094
1133
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeply-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Codeply from the terminal: an interactive AI coding agent, a local server (codeply serve), and a GitHub agent that runs in your own Actions runner.",
|
|
5
5
|
"main": "bin/codeply.js",
|
|
6
6
|
"bin": {
|