@turri-dev/cli 0.1.16 → 0.1.17-next.1ecfd4f
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/chat-commands.d.ts +25 -0
- package/dist/chat-commands.js +162 -0
- package/dist/chat-records.d.ts +44 -0
- package/dist/chat-records.js +102 -0
- package/dist/chat-scope.d.ts +14 -0
- package/dist/chat-scope.js +15 -0
- package/dist/chat-terminal.d.ts +5 -0
- package/dist/chat-terminal.js +136 -0
- package/dist/chat-ui.d.ts +33 -0
- package/dist/chat-ui.js +192 -0
- package/dist/chats.d.ts +176 -0
- package/dist/chats.js +356 -0
- package/dist/cli.js +5 -7
- package/dist/config.d.ts +8 -2
- package/dist/config.js +18 -4
- package/dist/extension.d.ts +20 -33
- package/dist/extension.js +348 -230
- package/dist/host-session.d.ts +51 -0
- package/dist/host-session.js +58 -0
- package/dist/host.d.ts +5 -4
- package/dist/host.js +6 -5
- package/dist/identity.js +5 -0
- package/dist/pi-launch.js +23 -1
- package/dist/regenerate/jobs.d.ts +2 -0
- package/dist/regenerate/jobs.js +3 -0
- package/dist/regenerate/ledger.d.ts +7 -0
- package/dist/regenerate/ledger.js +8 -1
- package/dist/regenerate/test-scripts.d.ts +1 -1
- package/dist/stacks.d.ts +2 -1
- package/dist/stacks.js +4 -3
- package/docs/README.md +4 -2
- package/docs/chats.md +73 -0
- package/docs/config.md +3 -3
- package/docs/development.md +4 -1
- package/docs/hosting.md +35 -28
- package/docs/using.md +2 -2
- package/package.json +1 -1
package/dist/extension.js
CHANGED
|
@@ -62,6 +62,8 @@ import { catalogueLine, chosenCommand, createCommandGate, startPicked, discoverS
|
|
|
62
62
|
import { supersededByTurri } from "./chat-skills.js";
|
|
63
63
|
import { conversationIdentity } from "./identity.js";
|
|
64
64
|
import { setSurface, surfaceOf } from "./surface.js";
|
|
65
|
+
import { createChats, scopedToChat } from "./chats.js";
|
|
66
|
+
import { registerChats } from "./chat-commands.js";
|
|
65
67
|
import { turriIdentity } from "./environment.js";
|
|
66
68
|
import { conformanceChooser } from "./regenerate/choose-conformance.js";
|
|
67
69
|
import { answerSideQuestion, askWithAsides, BTW_HOW, runningSkill, skillRunStarted, skillSessionText, transcriptOf } from "./btw.js";
|
|
@@ -201,33 +203,25 @@ function conversationSystemPrompt(base, host, blocks) {
|
|
|
201
203
|
const prompt = [base, conversationIdentity(host.surface), ...blocks].filter((part) => part !== "").join("\n\n");
|
|
202
204
|
return host.systemPrompt !== undefined ? host.systemPrompt(prompt) : prompt;
|
|
203
205
|
}
|
|
204
|
-
/**
|
|
206
|
+
/** Whether the person has been told which of pi's skills turri left out: once a session, not once a chat. */
|
|
207
|
+
let leftOutSaid = false;
|
|
208
|
+
/** The host each session's jobs report to. */
|
|
205
209
|
const hosts = new WeakMap();
|
|
206
|
-
/**
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
function
|
|
210
|
+
/**
|
|
211
|
+
* A turri session: its job registry and work loop, the ledger, and its chats, each with turri's extension in it
|
|
212
|
+
* (chats.ts). The terminal makes one as it starts (pi-launch.ts), a host with `createTurri` (host.ts); `persist`
|
|
213
|
+
* records the open chats for `turri --continue`.
|
|
214
|
+
*/
|
|
215
|
+
export function createTurriCore(host = {}, options = {}) {
|
|
212
216
|
dropNoise();
|
|
213
217
|
const cwd = process.cwd();
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
//
|
|
217
|
-
const panel = createJobPanel(() => shownJobs(jobs));
|
|
218
|
-
// Finished work is announced twice, to two audiences, because pi treats them differently.
|
|
219
|
-
// An entry is drawn the moment it is appended: the person sees the job end, now. The agent
|
|
220
|
-
// gets the full report as a hidden message that starts a turn of its own - at once when it is
|
|
221
|
-
// idle, else as soon as the turn it is in ends (a follow-up, never cutting into a reply or an
|
|
222
|
-
// interview) - so it reacts to every job that ends without waiting for the person to speak:
|
|
223
|
-
// does what the project's guidance says to do then (release what was built, say), or says
|
|
224
|
-
// what finished.
|
|
225
|
-
// What release has shipped, module by module: release does nothing for a module whose last commit it already shipped.
|
|
226
|
-
const deployState = createDeployState();
|
|
218
|
+
let core;
|
|
219
|
+
// Finished work is announced twice, to two audiences, because pi treats them differently - see ChatInstance.announce:
|
|
220
|
+
// in the chat that asked for it, or the one shown when that is closed.
|
|
227
221
|
const jobs = createJobRegistry({
|
|
228
222
|
// The loop's plan changed - a module being planned, or planned: the live panel shows it at once.
|
|
229
223
|
onNotify: () => {
|
|
230
|
-
|
|
224
|
+
core?.jobsChanged();
|
|
231
225
|
host.onPlanChange?.();
|
|
232
226
|
},
|
|
233
227
|
// A job that fails is run once more; failing again, it and every job behind it fail, and the
|
|
@@ -249,41 +243,145 @@ function turriExtension(pi, host) {
|
|
|
249
243
|
return diagnosis !== undefined ? describeDiagnosis(diagnosis) : undefined;
|
|
250
244
|
},
|
|
251
245
|
onChange: (job) => {
|
|
252
|
-
|
|
246
|
+
core?.jobsChanged();
|
|
253
247
|
host.onJobChange?.(job);
|
|
254
248
|
},
|
|
255
249
|
onFinish: (job) => {
|
|
256
|
-
|
|
250
|
+
core?.jobsChanged();
|
|
257
251
|
host.onJobChange?.(job);
|
|
258
252
|
// Queued again as another job: that one says how it went.
|
|
259
253
|
if (job.replacedBy !== undefined)
|
|
260
254
|
return;
|
|
261
|
-
announceJob(pi, job);
|
|
262
255
|
// Gone wrong: the next ctrl+h opens on it - the live panel that showed it is gone with the last pulse.
|
|
263
256
|
if (job.status === "failed" || job.status === "stopped" || didNotPass(job))
|
|
264
257
|
needsLooking.set(jobs, job.id);
|
|
265
|
-
|
|
266
|
-
if (job.specConflict !== undefined) {
|
|
267
|
-
void pi.sendMessage({ customType: "turri_spec_conflict", content: specConflictTask(job.specConflict), display: false, details: { jobId: job.id } }, { triggerTurn: true });
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
void pi.sendMessage({
|
|
271
|
-
customType: "turri_job",
|
|
272
|
-
content: (job.status === "succeeded"
|
|
273
|
-
? `Pulse ${job.id} (${job.kind}) finished after ${formatDuration(job)}.\n\n${job.report ?? ""}`
|
|
274
|
-
: job.status === "stopped"
|
|
275
|
-
? `Pulse ${job.id} (${job.kind}) was stopped after ${formatDuration(job)}. ${job.error ?? STOPPED_ON_REQUEST}${job.error === undefined || job.error === STOPPED_ON_REQUEST ? " The person stopped it; what it was building counts as not built." : ""}`
|
|
276
|
-
: `Pulse ${job.id} (${job.kind}) failed after ${formatDuration(job)}.\n\n${job.error ?? ""}`) +
|
|
277
|
-
`\n\n${JOB_FINISHED_REACTION}`,
|
|
278
|
-
display: false,
|
|
279
|
-
details: { jobId: job.id, status: job.status },
|
|
280
|
-
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
258
|
+
core?.jobFinished(job);
|
|
281
259
|
},
|
|
282
260
|
});
|
|
261
|
+
core = createChats({
|
|
262
|
+
cwd,
|
|
263
|
+
host,
|
|
264
|
+
jobs,
|
|
265
|
+
extension: (pi, link) => turriExtension(pi, core, link),
|
|
266
|
+
...(options.persist !== undefined ? { persist: options.persist } : {}),
|
|
267
|
+
...(options.dialogs !== undefined ? { dialogs: options.dialogs } : {}),
|
|
268
|
+
});
|
|
269
|
+
const session = core;
|
|
283
270
|
hosts.set(jobs, host);
|
|
284
271
|
// What the model is told of where the person watches the work, deep in the tools too: the host's surface, or the terminal.
|
|
285
272
|
setSurface(jobs, host.surface);
|
|
286
|
-
|
|
273
|
+
// What release has shipped, module by module: release does nothing for a module whose last commit it already shipped.
|
|
274
|
+
deployStates.set(jobs, createDeployState());
|
|
275
|
+
// What this session does, as facts, in the project's .turri/ledger - a pulse's own in its project. Said once, in the
|
|
276
|
+
// chat shown, when it cannot be written.
|
|
277
|
+
if (ledgerWanted()) {
|
|
278
|
+
startLedger({
|
|
279
|
+
mode: Object.keys(host).length > 0 ? "host" : "chat",
|
|
280
|
+
project: cwd,
|
|
281
|
+
warn: (message) => session.notify(message, "warning"),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
// What a turri that was killed left running - its commands' process groups - stopped as this one starts.
|
|
285
|
+
const orphans = sweepOrphanedGroups();
|
|
286
|
+
if (orphans > 0)
|
|
287
|
+
session.notify(`Stopped ${orphans} process group${orphans === 1 ? "" : "s"} a turri that was killed left running.`, "info");
|
|
288
|
+
// A pulse's provider failing and waited out, or a turn turri ended itself: said in the chat shown as it happens - the
|
|
289
|
+
// job panel's activity holds it too, but scrolls by.
|
|
290
|
+
onModelEvent((event) => {
|
|
291
|
+
const line = describeTurnIncident(event);
|
|
292
|
+
if (line !== undefined)
|
|
293
|
+
session.notify(event.job !== undefined ? `${event.job}: ${line}` : line, "warning");
|
|
294
|
+
});
|
|
295
|
+
// Sub-agents think as hard as the chat that started them, not pi's default - and reach the providers its extensions
|
|
296
|
+
// registered - a company's own gateway - which they load none of.
|
|
297
|
+
inheritThinkingLevel(() => session.instanceInScope()?.thinkingLevel());
|
|
298
|
+
inheritProviders(() => session.instanceInScope()?.context()?.modelRegistry);
|
|
299
|
+
return core;
|
|
300
|
+
}
|
|
301
|
+
/** The session pi builds turri's extension into when it is given turri's file rather than a session's factory. */
|
|
302
|
+
const SESSION = Symbol.for("turri.session");
|
|
303
|
+
/** The session of this process: pi-launch.ts's, or one made as pi first builds the extension (`pi -e src/extension.ts`). */
|
|
304
|
+
export function processSession(make = () => createTurriCore()) {
|
|
305
|
+
const global = globalThis;
|
|
306
|
+
return (global[SESSION] ??= make());
|
|
307
|
+
}
|
|
308
|
+
const extension = (pi) => processSession().extension(pi);
|
|
309
|
+
export default extension;
|
|
310
|
+
/**
|
|
311
|
+
* The session's work loop, for a host to read: made with the first chat's context - the models it plans with - when no
|
|
312
|
+
* chat has asked for work yet.
|
|
313
|
+
*/
|
|
314
|
+
export function sessionLoop(session) {
|
|
315
|
+
const known = workLoops.get(session.jobs)?.loop;
|
|
316
|
+
if (known !== undefined)
|
|
317
|
+
return known;
|
|
318
|
+
const live = session.shownChat()?.session ?? session.chats().map((info) => session.session(info.id)).find((found) => found !== undefined);
|
|
319
|
+
if (live === undefined)
|
|
320
|
+
throw new Error("No chat is open yet: the work loop plans with a chat's models.");
|
|
321
|
+
return workLoopFor(session.jobs, session.cwd, live.extensionRunner.createContext());
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* turri's extension, built into one chat of the session: its tools, commands and handlers run in the chat's scope -
|
|
325
|
+
* its stack, its id on the pulses it asks for - on the session's job registry and work loop.
|
|
326
|
+
*/
|
|
327
|
+
function turriExtension(api, session, link) {
|
|
328
|
+
const pi = scopedToChat(api, link.scope);
|
|
329
|
+
const { cwd, host, jobs } = session;
|
|
330
|
+
const dialogs = link.dialogs ?? host.dialogs;
|
|
331
|
+
// turri's own skills, and the project's - .turri/skills, .agents/skills (skills.ts).
|
|
332
|
+
const discoveredSkills = discoverSkills(PACKAGE_ROOT, cwd);
|
|
333
|
+
// What the background work is doing right now, drawn above the editor while it runs.
|
|
334
|
+
const panel = createJobPanel(() => shownJobs(jobs));
|
|
335
|
+
// The chat's context, as its tools and handlers were last given it: for its model and its providers.
|
|
336
|
+
let lastContext;
|
|
337
|
+
const contextNow = () => {
|
|
338
|
+
try {
|
|
339
|
+
void lastContext?.model;
|
|
340
|
+
return lastContext;
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
// Rebuilt by pi since - a /reload, a /fork: its new extension has the context now.
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
// Finished work is announced twice, to two audiences, because pi treats them differently.
|
|
348
|
+
// An entry is drawn the moment it is appended: the person sees the job end, now. The agent
|
|
349
|
+
// gets the full report as a hidden message that starts a turn of its own - at once when it is
|
|
350
|
+
// idle, else as soon as the turn it is in ends (a follow-up, never cutting into a reply or an
|
|
351
|
+
// interview) - so it reacts to every job that ends without waiting for the person to speak:
|
|
352
|
+
// does what the project's guidance says to do then (release what was built, say), or says
|
|
353
|
+
// what finished. In the chat that asked for the pulse, shown or not.
|
|
354
|
+
const announce = (job) => {
|
|
355
|
+
announceJob(pi, job);
|
|
356
|
+
// A spec that contradicts itself is the person's to settle, now: the agent is started on it.
|
|
357
|
+
if (job.specConflict !== undefined) {
|
|
358
|
+
void pi.sendMessage({ customType: "turri_spec_conflict", content: specConflictTask(job.specConflict), display: false, details: { jobId: job.id } }, { triggerTurn: true });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
void pi.sendMessage({
|
|
362
|
+
customType: "turri_job",
|
|
363
|
+
content: (job.status === "succeeded"
|
|
364
|
+
? `Pulse ${job.id} (${job.kind}) finished after ${formatDuration(job)}.\n\n${job.report ?? ""}`
|
|
365
|
+
: job.status === "stopped"
|
|
366
|
+
? `Pulse ${job.id} (${job.kind}) was stopped after ${formatDuration(job)}. ${job.error ?? STOPPED_ON_REQUEST}${job.error === undefined || job.error === STOPPED_ON_REQUEST ? " The person stopped it; what it was building counts as not built." : ""}`
|
|
367
|
+
: `Pulse ${job.id} (${job.kind}) failed after ${formatDuration(job)}.\n\n${job.error ?? ""}`) +
|
|
368
|
+
`\n\n${JOB_FINISHED_REACTION}`,
|
|
369
|
+
display: false,
|
|
370
|
+
details: { jobId: job.id, status: job.status },
|
|
371
|
+
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
372
|
+
};
|
|
373
|
+
link.live({
|
|
374
|
+
announce,
|
|
375
|
+
jobsChanged: () => panel.refresh(),
|
|
376
|
+
thinkingLevel: () => pi.getThinkingLevel(),
|
|
377
|
+
context: contextNow,
|
|
378
|
+
});
|
|
379
|
+
pi.on("session_start", (_event, ctx) => {
|
|
380
|
+
lastContext = ctx;
|
|
381
|
+
});
|
|
382
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
383
|
+
lastContext = ctx;
|
|
384
|
+
});
|
|
287
385
|
// The ***plain rules go into the system prompt in full, every turn. The block is
|
|
288
386
|
// identical each time, so it sits inside the cached prefix rather than invalidating it.
|
|
289
387
|
// With them, the instruction to do turri's work with its tools rather than its command
|
|
@@ -307,7 +405,6 @@ function turriExtension(pi, host) {
|
|
|
307
405
|
const machine = machineNeeds(cwd);
|
|
308
406
|
pi.on("session_start", () => machine.refreshSoon());
|
|
309
407
|
pi.on("agent_settled", () => machine.refreshSoon());
|
|
310
|
-
let leftOutSaid = false;
|
|
311
408
|
pi.on("before_agent_start", (event, ctx) => {
|
|
312
409
|
const offered = event.systemPromptOptions.skills;
|
|
313
410
|
const leftOut = offered.filter((skill) => supersededByTurri(skill, turriRuns));
|
|
@@ -378,66 +475,28 @@ function turriExtension(pi, host) {
|
|
|
378
475
|
};
|
|
379
476
|
pi.on("agent_end", endTurnCapture);
|
|
380
477
|
pi.on("agent_settled", endTurnCapture);
|
|
381
|
-
// A pulse's provider failing and waited out, or a turn turri ended itself: said in the chat as it happens - the job
|
|
382
|
-
// panel's activity holds it too, but scrolls by.
|
|
383
|
-
let stopTurnIncidents;
|
|
384
|
-
pi.on("session_start", (_event, ctx) => {
|
|
385
|
-
stopTurnIncidents?.();
|
|
386
|
-
if (!ctx.hasUI)
|
|
387
|
-
return;
|
|
388
|
-
stopTurnIncidents = onModelEvent((event) => {
|
|
389
|
-
const line = describeTurnIncident(event);
|
|
390
|
-
if (line !== undefined)
|
|
391
|
-
ctx.ui.notify(event.job !== undefined ? `${event.job}: ${line}` : line, "warning");
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
// What this session does, as facts, in the project's .turri/ledger - a pulse's own in its project. Said once, in the
|
|
395
|
-
// chat, when it cannot be written.
|
|
396
|
-
const ledgerTrouble = [];
|
|
397
|
-
let tellLedgerTrouble;
|
|
398
|
-
if (ledgerWanted()) {
|
|
399
|
-
startLedger({
|
|
400
|
-
mode: Object.keys(host).length > 0 ? "host" : "chat",
|
|
401
|
-
project: cwd,
|
|
402
|
-
warn: (message) => (tellLedgerTrouble !== undefined ? tellLedgerTrouble(message) : ledgerTrouble.push(message)),
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
478
|
pi.on("session_start", (_event, ctx) => {
|
|
406
479
|
if (!ctx.hasUI)
|
|
407
480
|
return;
|
|
408
|
-
tellLedgerTrouble = (message) => ctx.ui.notify(message, "warning");
|
|
409
|
-
for (const message of ledgerTrouble.splice(0))
|
|
410
|
-
tellLedgerTrouble(message);
|
|
411
|
-
});
|
|
412
|
-
// What a turri that was killed left running - its commands' process groups - stopped as this one starts.
|
|
413
|
-
const orphans = sweepOrphanedGroups();
|
|
414
|
-
let orphansSaid = orphans === 0;
|
|
415
|
-
pi.on("session_start", (_event, ctx) => {
|
|
416
|
-
if (orphansSaid || !ctx.hasUI)
|
|
417
|
-
return;
|
|
418
|
-
orphansSaid = true;
|
|
419
|
-
ctx.ui.notify(`Stopped ${orphans} process group${orphans === 1 ? "" : "s"} a turri that was killed left running.`, "info");
|
|
420
|
-
});
|
|
421
|
-
pi.on("session_start", (_event, ctx) => {
|
|
422
|
-
if (!ctx.hasUI)
|
|
423
|
-
return;
|
|
424
|
-
const ui = ctx.ui;
|
|
425
481
|
panel.attach({
|
|
426
|
-
setWidget: (key, content, options) =>
|
|
427
|
-
|
|
482
|
+
setWidget: (key, content, options) => {
|
|
483
|
+
try {
|
|
484
|
+
ctx.ui.setWidget(key, content, options);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
/* rebuilt by pi since: its new extension draws the panel */
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
getToolsExpanded: () => {
|
|
491
|
+
try {
|
|
492
|
+
return ctx.ui.getToolsExpanded();
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
},
|
|
428
498
|
});
|
|
429
499
|
});
|
|
430
|
-
// Sub-agents think as hard as the conversation that started them, not pi's default.
|
|
431
|
-
inheritThinkingLevel(() => pi.getThinkingLevel());
|
|
432
|
-
// And reach the providers its extensions registered - a company's own gateway - which they load none of.
|
|
433
|
-
let conversationModels;
|
|
434
|
-
pi.on("session_start", (_event, ctx) => {
|
|
435
|
-
conversationModels = ctx.modelRegistry;
|
|
436
|
-
});
|
|
437
|
-
pi.on("before_agent_start", (_event, ctx) => {
|
|
438
|
-
conversationModels = ctx.modelRegistry;
|
|
439
|
-
});
|
|
440
|
-
inheritProviders(() => conversationModels);
|
|
441
500
|
registerBranding(pi, discoveredSkills);
|
|
442
501
|
registerInterviewLog(pi);
|
|
443
502
|
registerJobTools(pi, jobs, cwd);
|
|
@@ -445,7 +504,7 @@ function turriExtension(pi, host) {
|
|
|
445
504
|
// once its command is typed, or picked among the options the conversation offers with ask_user.
|
|
446
505
|
const gate = createCommandGate();
|
|
447
506
|
// A skill picked is started at once, with what the person asked for: they never type its command after picking it.
|
|
448
|
-
registerCheckPlain(pi, cwd,
|
|
507
|
+
registerCheckPlain(pi, cwd, dialogs, (answer) => {
|
|
449
508
|
const chosen = chosenCommand(answer, discoveredSkills.skills);
|
|
450
509
|
if (chosen === undefined)
|
|
451
510
|
return undefined;
|
|
@@ -457,11 +516,13 @@ function turriExtension(pi, host) {
|
|
|
457
516
|
registerGlimpse(pi, cwd, jobs);
|
|
458
517
|
registerBind(pi, cwd, jobs);
|
|
459
518
|
registerRenderModels(pi);
|
|
460
|
-
deployStates.
|
|
461
|
-
registerDeploy(pi, cwd, jobs, deployState);
|
|
519
|
+
registerDeploy(pi, cwd, jobs, deployStates.get(jobs) ?? createDeployState());
|
|
462
520
|
registerSteering(pi, cwd);
|
|
463
521
|
registerSideQuestions(pi, cwd, jobs);
|
|
464
522
|
registerStacks(pi, cwd);
|
|
523
|
+
// More chats beside this one, in the terminal: /chat, alt+n, alt+1…9 (chat-commands.ts).
|
|
524
|
+
if (session.terminal)
|
|
525
|
+
registerChats(pi, session);
|
|
465
526
|
const { skills } = discoveredSkills;
|
|
466
527
|
if (skills.length === 0)
|
|
467
528
|
return;
|
|
@@ -643,7 +704,7 @@ function turriExtension(pi, host) {
|
|
|
643
704
|
push(` Q: ${question}`);
|
|
644
705
|
push(` A: ${answer}`);
|
|
645
706
|
recordExchange(pi, { skill: skill.name, question, answer, ...(said !== undefined && said.trim() !== "" ? { said: said.trim() } : {}) });
|
|
646
|
-
}, (question) => askOnTheSide(pi, ctx, cwd, jobs, question),
|
|
707
|
+
}, (question) => askOnTheSide(pi, ctx, cwd, jobs, question), dialogs),
|
|
647
708
|
model: conversationModel(ctx),
|
|
648
709
|
// Renders queued by a step land in the same registry as everything else, so
|
|
649
710
|
// pulses sees them without knowing a skill started them.
|
|
@@ -661,9 +722,14 @@ function turriExtension(pi, host) {
|
|
|
661
722
|
haltTool({ jobs, workLoop: () => workLoops.get(jobs)?.loop }),
|
|
662
723
|
conformTool({ cwd, loop: () => workLoopFor(jobs, cwd, ctx), model: (_skill, plainFile, named) => jobModel(ctx, "spec_changes", named, plainFile), wait: () => false }),
|
|
663
724
|
],
|
|
664
|
-
// The escalation becomes a real dialog
|
|
665
|
-
//
|
|
725
|
+
// The escalation becomes a real dialog - the host's own, when it has them. Without one there is nobody to ask,
|
|
726
|
+
// so the gate stands.
|
|
666
727
|
escalate: async ({ question, critique }) => {
|
|
728
|
+
if (dialogs !== undefined) {
|
|
729
|
+
const through = "Let it through";
|
|
730
|
+
const answer = await dialogs.select(`Review gate: ${skill.name} - ${question}`, [through, "Keep the gate"], critique);
|
|
731
|
+
return answer === through;
|
|
732
|
+
}
|
|
667
733
|
if (!ctx.hasUI) {
|
|
668
734
|
push(" no UI available for human review - gate stands");
|
|
669
735
|
return false;
|
|
@@ -793,22 +859,29 @@ function registerRegenerate(pi, cwd, jobs) {
|
|
|
793
859
|
? `Planning ${module}'s functionalit${again.length === 1 ? "y" : "ies"} ${again.join(", ")} again…`
|
|
794
860
|
: `Planning ${module}'s pulses…`, "info");
|
|
795
861
|
await nextFrame();
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
862
|
+
// Planned in the background: pi reads what is typed next only once a command returns, and planning takes the
|
|
863
|
+
// planner's time - the chat, and switching to another, stay free meanwhile. The plan is said once it is made.
|
|
864
|
+
const target = plainFile;
|
|
865
|
+
void (async () => {
|
|
866
|
+
try {
|
|
867
|
+
// Built with the stack chosen now - pinned to its pulses - or refused, a project of stacks built with none.
|
|
868
|
+
const statuses = await withCallStack(target, undefined, () => scratch
|
|
869
|
+
? loop.fromScratch(target, { ...request, chain })
|
|
870
|
+
: again.length > 0
|
|
871
|
+
? loop.regenerateAgain(target, again, request)
|
|
872
|
+
: loop.bringUp(target, request));
|
|
873
|
+
// Nothing to do: say how to build it again anyway.
|
|
874
|
+
const hint = statuses.length > 0 && statuses.every(isUpToDate)
|
|
875
|
+
? `\nTo regenerate parts of it anyway, name their functionalities - /regenerate ${basename(target)} 2.1 - or start over with /regenerate ${basename(target)} from scratch.`
|
|
876
|
+
: "";
|
|
877
|
+
say(describeModules(statuses) + hint, "info");
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
say(error instanceof Error ? error.message : String(error), "error");
|
|
881
|
+
}
|
|
882
|
+
})().catch(() => {
|
|
883
|
+
// The chat let go while it planned - closed, or rebuilt: there is nobody to say it to.
|
|
884
|
+
});
|
|
812
885
|
},
|
|
813
886
|
});
|
|
814
887
|
}
|
|
@@ -1141,7 +1214,7 @@ export function projectConfigBlock(root) {
|
|
|
1141
1214
|
return [
|
|
1142
1215
|
...settings(readProjectConfig(dir), withStack(undefined, () => findTestScripts(dir)), dir === root ? `${projectConfigFile(dir)}, for the specs here:` : `${relative(root, dir)}/${projectConfigFile(dir)}, for the specs in ${relative(root, dir)}/ - built in ${relative(root, dir)}/plain_modules:`, where, stacks.length > 0 ? "project of stacks" : "project"),
|
|
1143
1216
|
...(stacks.length > 0
|
|
1144
|
-
? ["", `Stacks: ${stacks.join(", ")} - chosen now: ${chosen ?? "none, so the project's file alone"}. Each module is built with the stack of its part, pinned to its pulses whatever is chosen after: a stack named after the module is its own - backend.plain is built with config.turri.backend.yaml - with nothing to choose. For a module whose stack has another name, pass it as stack to regenerate, conform, glimpse and change_plain. Choose one for
|
|
1217
|
+
? ["", `Stacks: ${stacks.join(", ")} - chosen now: ${chosen ?? "none, so the project's file alone"}. Each module is built with the stack of its part, pinned to its pulses whatever is chosen after: a stack named after the module is its own - backend.plain is built with config.turri.backend.yaml - with nothing to choose. For a module whose stack has another name, pass it as stack to regenerate, conform, glimpse and change_plain. Choose one for this chat with the stack tool only when the user asks.`]
|
|
1145
1218
|
: []),
|
|
1146
1219
|
// A stack: its file over the project's, for everything built while it is chosen.
|
|
1147
1220
|
...stacks.flatMap((stack) => [
|
|
@@ -1203,7 +1276,7 @@ export function projectConfigBlock(root) {
|
|
|
1203
1276
|
`<project config>`,
|
|
1204
1277
|
`A project's ${CONFIG_FILE} sets how its specs are built - and a stack's config.turri.<stack>.yaml, over it, how everything is built`,
|
|
1205
1278
|
"while that stack is chosen: its testing scripts, toolchains, models and conformance - a backend and a frontend each with their own scripts.",
|
|
1206
|
-
"One stack is chosen at a time, for
|
|
1279
|
+
"One stack is chosen at a time, in each chat for that chat: the render tools follow the project's file with the chosen stack's over it;",
|
|
1207
1280
|
`a project with no ${CONFIG_FILE} yet is read from its ${LEGACY_CONFIG_FILE} (and config.<stack>.yaml): to change a setting there, copy`,
|
|
1208
1281
|
`${LEGACY_CONFIG_FILE} to ${CONFIG_FILE} - and each config.<stack>.yaml to config.turri.<stack>.yaml - first, then edit the copy; never edit ${LEGACY_CONFIG_FILE}.`,
|
|
1209
1282
|
"answer questions about the project's settings from it, and say when a render will differ from it because the user",
|
|
@@ -1239,8 +1312,10 @@ function chatModelOf(ctx) {
|
|
|
1239
1312
|
}
|
|
1240
1313
|
}
|
|
1241
1314
|
let lastChatModel;
|
|
1242
|
-
/** The models chosen in this session, by kind of work. Saved in the
|
|
1315
|
+
/** The models chosen in this session, by kind of work - for every chat. Saved in the chat they were chosen in, so reopening it keeps them. */
|
|
1243
1316
|
const chosenModels = {};
|
|
1317
|
+
/** When they were last chosen: a chat reopened with older choices of its own leaves them be. */
|
|
1318
|
+
let chosenModelsAt = 0;
|
|
1244
1319
|
const MODEL_CHOICE_ENTRY = "turri_render_models";
|
|
1245
1320
|
/**
|
|
1246
1321
|
* The model a job's agents run on: the one named on the call, checked now so a wrong name fails
|
|
@@ -1355,6 +1430,7 @@ function setRenderModelTool(pi, conversation) {
|
|
|
1355
1430
|
}
|
|
1356
1431
|
chosenModels[params.work] = resolved.model;
|
|
1357
1432
|
}
|
|
1433
|
+
chosenModelsAt = Date.now();
|
|
1358
1434
|
pi.appendEntry(MODEL_CHOICE_ENTRY, { ...chosenModels });
|
|
1359
1435
|
const text = `The engine now:\n${describeChoices(chosenModels, chatModelOf(ctx), projectModelLabel(ctx))}`;
|
|
1360
1436
|
return { content: [{ type: "text", text }], details: { ...chosenModels } };
|
|
@@ -1363,14 +1439,24 @@ function setRenderModelTool(pi, conversation) {
|
|
|
1363
1439
|
}
|
|
1364
1440
|
/** Choosing which model renders what, for the rest of the session. */
|
|
1365
1441
|
function registerRenderModels(pi) {
|
|
1442
|
+
// The session's choices, every chat's: a chat reopened brings back what it chose, when that is newer than what the
|
|
1443
|
+
// session has - and a fresh chat starts with the session's.
|
|
1444
|
+
let started = false;
|
|
1366
1445
|
pi.on("session_start", (_event, ctx) => {
|
|
1367
|
-
|
|
1368
|
-
|
|
1446
|
+
if (started)
|
|
1447
|
+
return;
|
|
1448
|
+
started = true;
|
|
1369
1449
|
const saved = ctx.sessionManager
|
|
1370
1450
|
.getBranch()
|
|
1371
1451
|
.filter((entry) => entry.type === "custom" && entry.customType === MODEL_CHOICE_ENTRY)
|
|
1372
1452
|
.at(-1);
|
|
1373
|
-
|
|
1453
|
+
const at = Date.parse(saved?.timestamp ?? "") || 1;
|
|
1454
|
+
if (saved === undefined || at <= chosenModelsAt)
|
|
1455
|
+
return;
|
|
1456
|
+
chosenModelsAt = at;
|
|
1457
|
+
for (const key of Object.keys(chosenModels))
|
|
1458
|
+
delete chosenModels[key];
|
|
1459
|
+
Object.assign(chosenModels, saved.data ?? {});
|
|
1374
1460
|
});
|
|
1375
1461
|
pi.registerTool(setRenderModelTool(pi));
|
|
1376
1462
|
pi.registerCommand("engine", {
|
|
@@ -1426,7 +1512,6 @@ function workLoopFor(jobs, cwd, ctx) {
|
|
|
1426
1512
|
});
|
|
1427
1513
|
shared = { loop, from };
|
|
1428
1514
|
workLoops.set(jobs, shared);
|
|
1429
|
-
hosts.get(jobs)?.onWorkLoop?.(loop);
|
|
1430
1515
|
}
|
|
1431
1516
|
shared.from.ctx = ctx;
|
|
1432
1517
|
return shared.loop;
|
|
@@ -1646,6 +1731,133 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1646
1731
|
// screen - beside the chat, the key switches sides, giving the keys to the chat or the panel, as esc gives them to the
|
|
1647
1732
|
// chat, the panel staying; ctrl+c closes it.
|
|
1648
1733
|
// While the panel has the keys, every key is its own.
|
|
1734
|
+
// The side panel is the session's, one for every chat: it stays on the screen - beside the chat or over it - as
|
|
1735
|
+
// another chat is shown, and what it says to a chat it says to the one shown.
|
|
1736
|
+
const sidePanel = sidePanelOf(jobs, cwd);
|
|
1737
|
+
const openPanel = sidePanel.open;
|
|
1738
|
+
pi.on("session_start", () => {
|
|
1739
|
+
sidePanel.say = (text) => pi.sendUserMessage(text, { deliverAs: "followUp" });
|
|
1740
|
+
});
|
|
1741
|
+
// /pulses opens the side panel on every pulse, regenerating, planned and finished, and what each regenerates; regen -
|
|
1742
|
+
// the spec, with how far the code has been regenerated into it - is PANEL_KEY's, no command of its own beside
|
|
1743
|
+
// /regenerate. What the agents did is in each pulse's log.
|
|
1744
|
+
pi.registerCommand("pulses", {
|
|
1745
|
+
description: `The pulses, in the side panel: every pulse, regenerating, planned and finished, and what each regenerates. ${PANEL_KEY} opens the panel too.`,
|
|
1746
|
+
handler: async (_args, ctx) => {
|
|
1747
|
+
if (!ctx.hasUI)
|
|
1748
|
+
return;
|
|
1749
|
+
openPanel(ctx, "pulses");
|
|
1750
|
+
},
|
|
1751
|
+
});
|
|
1752
|
+
panelOpeners.set(jobs, (ctx) => openPanel(ctx));
|
|
1753
|
+
// A question open in the chat on the screen peeks at this chat's panel.
|
|
1754
|
+
const peek = {
|
|
1755
|
+
open: (ctx) => openPanel(ctx),
|
|
1756
|
+
shown: () => sidePanel.shown(),
|
|
1757
|
+
};
|
|
1758
|
+
questionPeek = peek;
|
|
1759
|
+
pi.on("session_start", () => {
|
|
1760
|
+
questionPeek = peek;
|
|
1761
|
+
});
|
|
1762
|
+
pi.registerShortcut(PANEL_KEY, {
|
|
1763
|
+
description: "Peek at regen and the pulses - a question waiting in the chat waits for you - or go back to it",
|
|
1764
|
+
handler: async (ctx) => {
|
|
1765
|
+
if (!ctx.hasUI)
|
|
1766
|
+
return;
|
|
1767
|
+
openPanel(ctx);
|
|
1768
|
+
},
|
|
1769
|
+
});
|
|
1770
|
+
// /turri-config: every setting of config.turri.yaml from the chat, each taking effect as it says - what turri fixed at its
|
|
1771
|
+
// start changed here, as it runs.
|
|
1772
|
+
registerTurriConfig(pi, {
|
|
1773
|
+
cwd,
|
|
1774
|
+
rules: () => loadRules(join(PACKAGE_ROOT, "genome", "rules")).map((rule) => rule.name),
|
|
1775
|
+
chatSpec: () => modelSpec(projectModels(readProjectConfig(cwd)).harness),
|
|
1776
|
+
live: {
|
|
1777
|
+
chatModel: async (spec, ctx) => {
|
|
1778
|
+
if (spec === undefined)
|
|
1779
|
+
return "The conversation keeps the model it runs on now; turri starts on pi's own choice next time.";
|
|
1780
|
+
const { model, thinking } = splitModelSpec(spec);
|
|
1781
|
+
if (model !== undefined) {
|
|
1782
|
+
const resolved = resolveModel(model, ctx.modelRegistry);
|
|
1783
|
+
if (!resolved.ok)
|
|
1784
|
+
return `The conversation keeps its model: ${model} cannot be used here.\n${describeResolution(resolved)}`;
|
|
1785
|
+
const slash = resolved.model.indexOf("/");
|
|
1786
|
+
const found = ctx.modelRegistry.find(resolved.model.slice(0, slash), resolved.model.slice(slash + 1));
|
|
1787
|
+
if (found === undefined || !(await pi.setModel(found)))
|
|
1788
|
+
return `The conversation keeps its model: ${resolved.model} could not be switched to.`;
|
|
1789
|
+
}
|
|
1790
|
+
if (thinking !== undefined)
|
|
1791
|
+
pi.setThinkingLevel(thinking);
|
|
1792
|
+
return `The conversation runs on ${spec} now.`;
|
|
1793
|
+
},
|
|
1794
|
+
codingModel: () => {
|
|
1795
|
+
const dropped = [];
|
|
1796
|
+
// What the session chose wins over the file; changed in the file, the file's holds from now on.
|
|
1797
|
+
if (process.env[CODING_MODEL_ENV] !== undefined) {
|
|
1798
|
+
dropped.push(`--coding-model ${process.env[CODING_MODEL_ENV]}`);
|
|
1799
|
+
delete process.env[CODING_MODEL_ENV];
|
|
1800
|
+
}
|
|
1801
|
+
chosenModelsAt = Date.now();
|
|
1802
|
+
for (const work of Object.keys(chosenModels)) {
|
|
1803
|
+
dropped.push(`the engine's ${work === "all" ? "" : `${work.replace(/_/g, " ")} `}model ${chosenModels[work]}`);
|
|
1804
|
+
delete chosenModels[work];
|
|
1805
|
+
}
|
|
1806
|
+
return dropped.length === 0 ? undefined : `Chosen this session, and given way to the config: ${dropped.join("; ")}.`;
|
|
1807
|
+
},
|
|
1808
|
+
parallelJobs: (count) => {
|
|
1809
|
+
const limit = count ?? parallelJobs(cwd);
|
|
1810
|
+
jobs.setMaxRunning(limit);
|
|
1811
|
+
return `Up to ${limit} pulse${limit === 1 ? "" : "s"} run at once now.`;
|
|
1812
|
+
},
|
|
1813
|
+
},
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
export { fromWorkingDir };
|
|
1817
|
+
const WATCH_REDRAW_MS = 500;
|
|
1818
|
+
/**
|
|
1819
|
+
* What the agent is to do with a job that just ended, unasked: what the project's guidance says to do then, or else say
|
|
1820
|
+
* in a line or two what ended - the person saw it end already, in the transcript.
|
|
1821
|
+
*/
|
|
1822
|
+
export const JOB_FINISHED_REACTION = [
|
|
1823
|
+
"React to this now - the person has not asked, and need not. If guidance for this project - its AGENTS.md, a skill,",
|
|
1824
|
+
"what the person said earlier - says what to do when a pulse finishes (release what was built, run something, tell",
|
|
1825
|
+
"someone), do that, as it says. Otherwise say in a line or two what finished and what it means for the person - the",
|
|
1826
|
+
"report is theirs to read in full; do not repeat it - and, when it failed, what they can do. Start no other work.",
|
|
1827
|
+
"A report that says \"Released after the pulse\" was released by turri itself, as the pulse's last stage: do not call",
|
|
1828
|
+
"release for it. Say in a line what was released, or why nothing was - then, when it was released, do what you were",
|
|
1829
|
+
"told to do once something is released (opening it where the person can watch it, saying what to test), if anything.",
|
|
1830
|
+
"A pulse that stopped on its environment - \"stopped: turri's sandbox kept its agents from…\" or \"stopped: the",
|
|
1831
|
+
"environment lacks…\" - stops the same way again until that is fixed: never regenerate it before. Heal it, then regenerate the",
|
|
1832
|
+
"module to resume:",
|
|
1833
|
+
"- turri's sandbox kept the agents from reading a folder on this machine: nothing is missing. Add the folder its report",
|
|
1834
|
+
" names to `toolchains:` in config.turri.yaml - a list, keeping what is there - say so in a line, and resume.",
|
|
1835
|
+
"- turri's sandbox kept the agents from writing a file - a lock a build tool takes, say: no setting lets them, and",
|
|
1836
|
+
" `toolchains:` does not - never add it there, and never resume before the fix. Do what the report says if it is the",
|
|
1837
|
+
" project's to change - a tool told to use `$HOME` in the build's own files - say so, and resume; otherwise tell the",
|
|
1838
|
+
" person, in a line or two, what the report says and that \"resume\" picks it up once it is done.",
|
|
1839
|
+
"- the machine lacks a toolchain or a tool - a JDK, Maven, Gradle, Go, uv, Node, a system library: first find out, in your",
|
|
1840
|
+
" shell, whether it is installed somewhere off the PATH (`command -v`, Homebrew's opt folders, /Library/Java/",
|
|
1841
|
+
" JavaVirtualMachines, a version manager the person uses - mise, asdf, sdkman, nvm). Then ask the person with ask_user",
|
|
1842
|
+
" to let you put it in place the way this machine installs things - the exact command, `brew install openjdk@21`,",
|
|
1843
|
+
" `mise use -g java@21`, `sdk install maven` - and on yes run it in your shell, check that the command is found now as",
|
|
1844
|
+
" the testing script would find it (`command -v`, then the doctor tool, which looks on turri's own PATH), and resume. A tool installed where the PATH turri",
|
|
1845
|
+
" was started with does not reach needs turri restarted: say so, with the line for the shell profile. On no, or when it",
|
|
1846
|
+
" needs what you cannot do - sudo with a password, a licence, a download behind a login - tell the person exactly what",
|
|
1847
|
+
" to do, and that \"resume\" picks it up once it is in.",
|
|
1848
|
+
"- a credential `agent-env:` names that is not there: never make one up - tell the person where it is read from.",
|
|
1849
|
+
].join("\n");
|
|
1850
|
+
const sidePanels = new WeakMap();
|
|
1851
|
+
/** The side panel of the session these jobs are, made as a chat first asks for it. */
|
|
1852
|
+
function sidePanelOf(jobs, cwd) {
|
|
1853
|
+
const known = sidePanels.get(jobs);
|
|
1854
|
+
if (known !== undefined)
|
|
1855
|
+
return known;
|
|
1856
|
+
const self = {
|
|
1857
|
+
open: (ctx, view) => openPanel(ctx, view),
|
|
1858
|
+
shown: () => panel !== undefined && panel.keys,
|
|
1859
|
+
};
|
|
1860
|
+
sidePanels.set(jobs, self);
|
|
1649
1861
|
let panel;
|
|
1650
1862
|
// Beside the chat or over it: as the person last left it, for the conversation.
|
|
1651
1863
|
let panelDocked = false;
|
|
@@ -1797,7 +2009,7 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1797
2009
|
discuss: (page) => {
|
|
1798
2010
|
back();
|
|
1799
2011
|
const why = page.codebase?.stuck?.why;
|
|
1800
|
-
|
|
2012
|
+
self.say?.(`regen says ${page.module} needs me: ${page.status}. What went wrong, and what do we do about it?${why !== undefined ? `\n\n(turri's reason: ${why})` : ""}`);
|
|
1801
2013
|
},
|
|
1802
2014
|
// Something took the keys' focus while the panel showed - a question in the chat - and waits there.
|
|
1803
2015
|
waiting: () => {
|
|
@@ -1914,109 +2126,8 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1914
2126
|
},
|
|
1915
2127
|
});
|
|
1916
2128
|
};
|
|
1917
|
-
|
|
1918
|
-
// the spec, with how far the code has been regenerated into it - is PANEL_KEY's, no command of its own beside
|
|
1919
|
-
// /regenerate. What the agents did is in each pulse's log.
|
|
1920
|
-
pi.registerCommand("pulses", {
|
|
1921
|
-
description: `The pulses, in the side panel: every pulse, regenerating, planned and finished, and what each regenerates. ${PANEL_KEY} opens the panel too.`,
|
|
1922
|
-
handler: async (_args, ctx) => {
|
|
1923
|
-
if (!ctx.hasUI)
|
|
1924
|
-
return;
|
|
1925
|
-
openPanel(ctx, "pulses");
|
|
1926
|
-
},
|
|
1927
|
-
});
|
|
1928
|
-
panelOpeners.set(jobs, (ctx) => openPanel(ctx));
|
|
1929
|
-
questionPeek = {
|
|
1930
|
-
open: (ctx) => openPanel(ctx),
|
|
1931
|
-
shown: () => panel !== undefined && panel.keys,
|
|
1932
|
-
};
|
|
1933
|
-
pi.registerShortcut(PANEL_KEY, {
|
|
1934
|
-
description: "Peek at regen and the pulses - a question waiting in the chat waits for you - or go back to it",
|
|
1935
|
-
handler: async (ctx) => {
|
|
1936
|
-
if (!ctx.hasUI)
|
|
1937
|
-
return;
|
|
1938
|
-
openPanel(ctx);
|
|
1939
|
-
},
|
|
1940
|
-
});
|
|
1941
|
-
// /turri-config: every setting of config.turri.yaml from the chat, each taking effect as it says - what turri fixed at its
|
|
1942
|
-
// start changed here, as it runs.
|
|
1943
|
-
registerTurriConfig(pi, {
|
|
1944
|
-
cwd,
|
|
1945
|
-
rules: () => loadRules(join(PACKAGE_ROOT, "genome", "rules")).map((rule) => rule.name),
|
|
1946
|
-
chatSpec: () => modelSpec(projectModels(readProjectConfig(cwd)).harness),
|
|
1947
|
-
live: {
|
|
1948
|
-
chatModel: async (spec, ctx) => {
|
|
1949
|
-
if (spec === undefined)
|
|
1950
|
-
return "The conversation keeps the model it runs on now; turri starts on pi's own choice next time.";
|
|
1951
|
-
const { model, thinking } = splitModelSpec(spec);
|
|
1952
|
-
if (model !== undefined) {
|
|
1953
|
-
const resolved = resolveModel(model, ctx.modelRegistry);
|
|
1954
|
-
if (!resolved.ok)
|
|
1955
|
-
return `The conversation keeps its model: ${model} cannot be used here.\n${describeResolution(resolved)}`;
|
|
1956
|
-
const slash = resolved.model.indexOf("/");
|
|
1957
|
-
const found = ctx.modelRegistry.find(resolved.model.slice(0, slash), resolved.model.slice(slash + 1));
|
|
1958
|
-
if (found === undefined || !(await pi.setModel(found)))
|
|
1959
|
-
return `The conversation keeps its model: ${resolved.model} could not be switched to.`;
|
|
1960
|
-
}
|
|
1961
|
-
if (thinking !== undefined)
|
|
1962
|
-
pi.setThinkingLevel(thinking);
|
|
1963
|
-
return `The conversation runs on ${spec} now.`;
|
|
1964
|
-
},
|
|
1965
|
-
codingModel: () => {
|
|
1966
|
-
const dropped = [];
|
|
1967
|
-
// What the session chose wins over the file; changed in the file, the file's holds from now on.
|
|
1968
|
-
if (process.env[CODING_MODEL_ENV] !== undefined) {
|
|
1969
|
-
dropped.push(`--coding-model ${process.env[CODING_MODEL_ENV]}`);
|
|
1970
|
-
delete process.env[CODING_MODEL_ENV];
|
|
1971
|
-
}
|
|
1972
|
-
for (const work of Object.keys(chosenModels)) {
|
|
1973
|
-
dropped.push(`the engine's ${work === "all" ? "" : `${work.replace(/_/g, " ")} `}model ${chosenModels[work]}`);
|
|
1974
|
-
delete chosenModels[work];
|
|
1975
|
-
}
|
|
1976
|
-
return dropped.length === 0 ? undefined : `Chosen this session, and given way to the config: ${dropped.join("; ")}.`;
|
|
1977
|
-
},
|
|
1978
|
-
parallelJobs: (count) => {
|
|
1979
|
-
const limit = count ?? parallelJobs(cwd);
|
|
1980
|
-
jobs.setMaxRunning(limit);
|
|
1981
|
-
return `Up to ${limit} pulse${limit === 1 ? "" : "s"} run at once now.`;
|
|
1982
|
-
},
|
|
1983
|
-
},
|
|
1984
|
-
});
|
|
2129
|
+
return self;
|
|
1985
2130
|
}
|
|
1986
|
-
export { fromWorkingDir };
|
|
1987
|
-
const WATCH_REDRAW_MS = 500;
|
|
1988
|
-
/**
|
|
1989
|
-
* What the agent is to do with a job that just ended, unasked: what the project's guidance says to do then, or else say
|
|
1990
|
-
* in a line or two what ended - the person saw it end already, in the transcript.
|
|
1991
|
-
*/
|
|
1992
|
-
export const JOB_FINISHED_REACTION = [
|
|
1993
|
-
"React to this now - the person has not asked, and need not. If guidance for this project - its AGENTS.md, a skill,",
|
|
1994
|
-
"what the person said earlier - says what to do when a pulse finishes (release what was built, run something, tell",
|
|
1995
|
-
"someone), do that, as it says. Otherwise say in a line or two what finished and what it means for the person - the",
|
|
1996
|
-
"report is theirs to read in full; do not repeat it - and, when it failed, what they can do. Start no other work.",
|
|
1997
|
-
"A report that says \"Released after the pulse\" was released by turri itself, as the pulse's last stage: do not call",
|
|
1998
|
-
"release for it. Say in a line what was released, or why nothing was - then, when it was released, do what you were",
|
|
1999
|
-
"told to do once something is released (opening it where the person can watch it, saying what to test), if anything.",
|
|
2000
|
-
"A pulse that stopped on its environment - \"stopped: turri's sandbox kept its agents from…\" or \"stopped: the",
|
|
2001
|
-
"environment lacks…\" - stops the same way again until that is fixed: never regenerate it before. Heal it, then regenerate the",
|
|
2002
|
-
"module to resume:",
|
|
2003
|
-
"- turri's sandbox kept the agents from reading a folder on this machine: nothing is missing. Add the folder its report",
|
|
2004
|
-
" names to `toolchains:` in config.turri.yaml - a list, keeping what is there - say so in a line, and resume.",
|
|
2005
|
-
"- turri's sandbox kept the agents from writing a file - a lock a build tool takes, say: no setting lets them, and",
|
|
2006
|
-
" `toolchains:` does not - never add it there, and never resume before the fix. Do what the report says if it is the",
|
|
2007
|
-
" project's to change - a tool told to use `$HOME` in the build's own files - say so, and resume; otherwise tell the",
|
|
2008
|
-
" person, in a line or two, what the report says and that \"resume\" picks it up once it is done.",
|
|
2009
|
-
"- the machine lacks a toolchain or a tool - a JDK, Maven, Gradle, Go, uv, Node, a system library: first find out, in your",
|
|
2010
|
-
" shell, whether it is installed somewhere off the PATH (`command -v`, Homebrew's opt folders, /Library/Java/",
|
|
2011
|
-
" JavaVirtualMachines, a version manager the person uses - mise, asdf, sdkman, nvm). Then ask the person with ask_user",
|
|
2012
|
-
" to let you put it in place the way this machine installs things - the exact command, `brew install openjdk@21`,",
|
|
2013
|
-
" `mise use -g java@21`, `sdk install maven` - and on yes run it in your shell, check that the command is found now as",
|
|
2014
|
-
" the testing script would find it (`command -v`, then the doctor tool, which looks on turri's own PATH), and resume. A tool installed where the PATH turri",
|
|
2015
|
-
" was started with does not reach needs turri restarted: say so, with the line for the shell profile. On no, or when it",
|
|
2016
|
-
" needs what you cannot do - sudo with a password, a licence, a download behind a login - tell the person exactly what",
|
|
2017
|
-
" to do, and that \"resume\" picks it up once it is in.",
|
|
2018
|
-
"- a credential `agent-env:` names that is not there: never make one up - tell the person where it is read from.",
|
|
2019
|
-
].join("\n");
|
|
2020
2131
|
/** Opens the side panel, and goes back to it from the conversation. */
|
|
2021
2132
|
/** The modules in this project that have a history, by name. */
|
|
2022
2133
|
function modulesWithHistory(cwd) {
|
|
@@ -2225,8 +2336,10 @@ function registerDeploy(pi, cwd, jobs, state) {
|
|
|
2225
2336
|
});
|
|
2226
2337
|
for (const tool of tools)
|
|
2227
2338
|
pi.registerTool(tool);
|
|
2228
|
-
|
|
2229
|
-
|
|
2339
|
+
// What activate started runs until turri ends - not when a chat is opened, closed, forked or reloaded.
|
|
2340
|
+
pi.on("session_shutdown", async (event) => {
|
|
2341
|
+
if (event.reason === "quit")
|
|
2342
|
+
await stopDeployed();
|
|
2230
2343
|
});
|
|
2231
2344
|
}
|
|
2232
2345
|
/** validate, for the conversation and for the skills that write specs alike. */
|
|
@@ -2480,11 +2593,16 @@ function registerBranding(pi, skills) {
|
|
|
2480
2593
|
// where the terminal shows images, drawn in text where it does not.
|
|
2481
2594
|
return new Banner(lines, (rule) => theme.fg("borderMuted", rule), jellyfishArt(theme.appearance, (theme.getColorMode?.() ?? "truecolor") === "truecolor"));
|
|
2482
2595
|
});
|
|
2596
|
+
let bannered = false;
|
|
2483
2597
|
pi.on("session_start", (_event, ctx) => {
|
|
2484
2598
|
if (ctx.hasUI) {
|
|
2485
2599
|
padTheScreen(ctx);
|
|
2486
2600
|
animateJellyfish(ctx);
|
|
2487
2601
|
}
|
|
2602
|
+
// Once a chat: it starts again each time it is shown.
|
|
2603
|
+
if (bannered)
|
|
2604
|
+
return;
|
|
2605
|
+
bannered = true;
|
|
2488
2606
|
try {
|
|
2489
2607
|
const identity = turriIdentity();
|
|
2490
2608
|
pi.appendEntry(BANNER_ENTRY, {
|