@turri-dev/cli 0.1.26-next.2fc445f → 0.1.26-next.f80f259
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-records.d.ts +4 -0
- package/dist/chat-records.js +5 -0
- package/dist/chats.d.ts +17 -7
- package/dist/chats.js +168 -26
- package/dist/extension.js +6 -7
- package/dist/host-session.d.ts +4 -2
- package/dist/notice.d.ts +18 -2
- package/dist/notice.js +10 -1
- package/dist/regenerate/ledger.d.ts +4 -0
- package/dist/sentinel-chat.js +20 -2
- package/dist/sentinel.js +17 -6
- package/docs/chats.md +1 -1
- package/docs/hosting.md +6 -3
- package/docs/sentinel.md +14 -9
- package/docs/using.md +1 -1
- package/package.json +1 -1
package/dist/chat-records.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ export interface RunRecord {
|
|
|
24
24
|
/** How many runs' chats are kept: the most recent, the one `--continue` reopens, among them. */
|
|
25
25
|
export declare const RUNS_KEPT = 5;
|
|
26
26
|
export declare function chatsFile(project: string): string;
|
|
27
|
+
/** A chat's saved conversation it no longer has - the sentinel's, before a pulse's fresh one - deleted, unless kept. */
|
|
28
|
+
export declare function forgetSessionFile(file: string, options?: {
|
|
29
|
+
keep?: boolean;
|
|
30
|
+
}): void;
|
|
27
31
|
/** The runs recorded here, the most recent first. */
|
|
28
32
|
export declare function recordedRuns(project: string): RunRecord[];
|
|
29
33
|
/**
|
package/dist/chat-records.js
CHANGED
|
@@ -31,6 +31,11 @@ function write(project, file) {
|
|
|
31
31
|
// A chat not recorded is not reopened; nothing else is lost.
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
+
/** A chat's saved conversation it no longer has - the sentinel's, before a pulse's fresh one - deleted, unless kept. */
|
|
35
|
+
export function forgetSessionFile(file, options = {}) {
|
|
36
|
+
if (options.keep !== true)
|
|
37
|
+
deleteSession(file);
|
|
38
|
+
}
|
|
34
39
|
function deleteSession(file) {
|
|
35
40
|
try {
|
|
36
41
|
rmSync(file, { force: true });
|
package/dist/chats.d.ts
CHANGED
|
@@ -24,9 +24,10 @@ import { type ChatRole, type Conclusion, type Notice, type NoticeAnswer, type No
|
|
|
24
24
|
*
|
|
25
25
|
* With the project's sentinel on (sentinel.ts), every finished pulse goes to the sentinel - a chat of the session's own,
|
|
26
26
|
* opened in the background with the first, and again after it is closed - and the chat the person looks at shows only
|
|
27
|
-
* that it landed.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* that it landed. The sentinel checks each pulse in a conversation of its own: its chat keeps its id, and is given a new
|
|
28
|
+
* session for each pulse, one at a time. What the sentinel finds wrong it puts to the person as a notice, in the chat
|
|
29
|
+
* they look at. Under `sentinel: ask`, the person is asked first - a notice of turri's own, before it first opens in the
|
|
30
|
+
* session - the pulses that end meanwhile held until they answer.
|
|
30
31
|
*/
|
|
31
32
|
/** The most chats a session has open at once. */
|
|
32
33
|
export declare const MAX_CHATS = 9;
|
|
@@ -62,6 +63,11 @@ export interface TurriHost {
|
|
|
62
63
|
* work: what it returns is what is sent. For a host that needs to change anything else.
|
|
63
64
|
*/
|
|
64
65
|
systemPrompt?(prompt: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* A chat's session replaced by a new one, under the same id: the sentinel's, a fresh conversation for each pulse it
|
|
68
|
+
* checks - told before the pulse is put to it. The host follows this session in place of the one it had.
|
|
69
|
+
*/
|
|
70
|
+
onChatSession?(chat: string, session: AgentSession): void;
|
|
65
71
|
/** The sentinel is done with a pulse - told to the chat it names, whose agent says so. */
|
|
66
72
|
onConcluded?(conclusion: Conclusion & {
|
|
67
73
|
chat: string;
|
|
@@ -91,11 +97,10 @@ export interface ChatInstance {
|
|
|
91
97
|
landed(job: Job, options?: {
|
|
92
98
|
tell?: boolean;
|
|
93
99
|
}): void;
|
|
94
|
-
/** The sentinel's: a pulse ended - its agent
|
|
100
|
+
/** The sentinel's: a pulse ended - its agent, in a conversation of its own for this pulse, told of it. */
|
|
95
101
|
brief(job: Job, context: {
|
|
96
102
|
asked?: string;
|
|
97
103
|
focused?: string;
|
|
98
|
-
replace: boolean;
|
|
99
104
|
}): Promise<void>;
|
|
100
105
|
/** A notice put in the chat: drawn, and - in the terminal - put to the person once the chat is idle. */
|
|
101
106
|
notice(notice: Notice): void;
|
|
@@ -210,8 +215,11 @@ export interface TurriCore {
|
|
|
210
215
|
concluded(conclusion: Conclusion): string | undefined;
|
|
211
216
|
/** The notices put to the person and not yet answered. */
|
|
212
217
|
notices(): Notice[];
|
|
213
|
-
/**
|
|
214
|
-
|
|
218
|
+
/**
|
|
219
|
+
* A notice answered: what it asked done in the chat it is in, dismissed, or - an ignorable one - ignored for good.
|
|
220
|
+
* `refused`: `ignore` on one that is not ignorable.
|
|
221
|
+
*/
|
|
222
|
+
answer(id: string, answer: NoticeAnswer): "done" | "dismissed" | "ignored" | "refused" | "missing" | "over";
|
|
215
223
|
/** For the extension: the jobs changed - every chat draws them again. */
|
|
216
224
|
jobsChanged(): void;
|
|
217
225
|
/** The chat whose work this is, else the one shown: its instance. */
|
|
@@ -238,6 +246,8 @@ export interface CoreOptions {
|
|
|
238
246
|
sentinelStart?: (job: Job) => NoticeInput;
|
|
239
247
|
/** The person chose for the project, asked whether to start the sentinel: `sentinel:` written. */
|
|
240
248
|
setSentinel?: (on: boolean) => void;
|
|
249
|
+
/** The person ignored a notice for good: kept in the sentinel's guidance, for every conversation after. */
|
|
250
|
+
ignored?: (notice: Notice) => void;
|
|
241
251
|
}
|
|
242
252
|
/** A chat's session, as stopping what it does reaches it. */
|
|
243
253
|
export type Stoppable = Pick<AgentSession, "isStreaming" | "isCompacting" | "isRetrying" | "isBashRunning" | "abort" | "abortBash">;
|
package/dist/chats.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { currentChat, inChat } from "./chat-scope.js";
|
|
4
|
-
import { adoptRun, forgetChat, recordRun } from "./chat-records.js";
|
|
4
|
+
import { adoptRun, forgetChat, forgetSessionFile, recordRun } from "./chat-records.js";
|
|
5
5
|
import { createChatUi } from "./chat-ui.js";
|
|
6
6
|
import { debugMode } from "./config.js";
|
|
7
7
|
import { record } from "./regenerate/ledger.js";
|
|
@@ -23,9 +23,10 @@ import { newNotice, noticeTask, SENTINEL, sentinelStartChoice, wentWrong } from
|
|
|
23
23
|
*
|
|
24
24
|
* With the project's sentinel on (sentinel.ts), every finished pulse goes to the sentinel - a chat of the session's own,
|
|
25
25
|
* opened in the background with the first, and again after it is closed - and the chat the person looks at shows only
|
|
26
|
-
* that it landed.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* that it landed. The sentinel checks each pulse in a conversation of its own: its chat keeps its id, and is given a new
|
|
27
|
+
* session for each pulse, one at a time. What the sentinel finds wrong it puts to the person as a notice, in the chat
|
|
28
|
+
* they look at. Under `sentinel: ask`, the person is asked first - a notice of turri's own, before it first opens in the
|
|
29
|
+
* session - the pulses that end meanwhile held until they answer.
|
|
29
30
|
*/
|
|
30
31
|
/** The most chats a session has open at once. */
|
|
31
32
|
export const MAX_CHATS = 9;
|
|
@@ -90,8 +91,12 @@ export function createChats(options) {
|
|
|
90
91
|
let notices = [];
|
|
91
92
|
/** What the sentinel put to the person, and turri itself, this session: open until answered or superseded. */
|
|
92
93
|
const open = [];
|
|
93
|
-
/** The pulse the sentinel is on
|
|
94
|
-
let
|
|
94
|
+
/** The pulse the sentinel's conversation is on, from its brief until its agent is done: a newer one of the same module replaces a check of one that went well. */
|
|
95
|
+
let checking;
|
|
96
|
+
/** Pulses that finished while the sentinel was at work: each checked in turn, in a conversation of its own. */
|
|
97
|
+
const waiting = [];
|
|
98
|
+
/** What the sentinel is given is given one pulse at a time: its conversations made, and its pulses put to them, in order. */
|
|
99
|
+
let sentinelWork = Promise.resolve();
|
|
95
100
|
/**
|
|
96
101
|
* Under `sentinel: ask`, whether the person said to start the sentinel this session: asked - the notice `asking` open,
|
|
97
102
|
* the pulses that ended meanwhile `held` - started, or not this session.
|
|
@@ -174,12 +179,15 @@ export function createChats(options) {
|
|
|
174
179
|
}
|
|
175
180
|
else if (event.type === "agent_end") {
|
|
176
181
|
chat.working = false;
|
|
177
|
-
if (chat.role === SENTINEL)
|
|
178
|
-
sentinelOnJob = undefined;
|
|
179
182
|
if (shownId !== chat.id)
|
|
180
183
|
chat.unread = true;
|
|
181
184
|
persist();
|
|
182
185
|
changed();
|
|
186
|
+
// Done with its pulse - or with what the person said to it: the next pulse waiting, in a conversation of its own.
|
|
187
|
+
if (chat.role === SENTINEL && event.willRetry !== true) {
|
|
188
|
+
checking = undefined;
|
|
189
|
+
toSentinel(() => nextPulse(chat));
|
|
190
|
+
}
|
|
183
191
|
}
|
|
184
192
|
else if (event.type === "session_info_changed") {
|
|
185
193
|
persist();
|
|
@@ -264,6 +272,11 @@ export function createChats(options) {
|
|
|
264
272
|
}
|
|
265
273
|
const recorded = run.chats.find((kept) => kept.id === id);
|
|
266
274
|
chats.splice(chats.indexOf(chat), 1);
|
|
275
|
+
// Closed, the sentinel is let go with what it had to check: the next pulse that finishes opens it again.
|
|
276
|
+
if (chat.role === SENTINEL) {
|
|
277
|
+
checking = undefined;
|
|
278
|
+
waiting.length = 0;
|
|
279
|
+
}
|
|
267
280
|
// The question whether to start the sentinel was in it: put again in the chat the person looks at now.
|
|
268
281
|
if (asking?.chat === id)
|
|
269
282
|
askAgain();
|
|
@@ -394,7 +407,9 @@ export function createChats(options) {
|
|
|
394
407
|
for (const older of open.filter((notice) => notice.state === "open" && notice.module === input.module))
|
|
395
408
|
over(older, "superseded");
|
|
396
409
|
}
|
|
397
|
-
|
|
410
|
+
// Only the sentinel's own may be ignored: never the question whether to start it.
|
|
411
|
+
const { ignorable, ...rest } = input;
|
|
412
|
+
const notice = newNotice(ignorable === true && input.kind !== "sentinel-start" ? input : rest, target.id);
|
|
398
413
|
open.push(notice);
|
|
399
414
|
if (target.id !== shownId)
|
|
400
415
|
target.unread = true;
|
|
@@ -447,6 +462,19 @@ export function createChats(options) {
|
|
|
447
462
|
return "missing";
|
|
448
463
|
if (notice.state !== "open")
|
|
449
464
|
return "over";
|
|
465
|
+
// Left as the build has it, for good: nothing done in the chat, and the sentinel told not to raise it again.
|
|
466
|
+
if ("ignore" in answer) {
|
|
467
|
+
if (notice.ignorable !== true)
|
|
468
|
+
return "refused";
|
|
469
|
+
try {
|
|
470
|
+
options.ignored?.(notice);
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
core.notify(`The sentinel's guidance could not be written: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
474
|
+
}
|
|
475
|
+
over(notice, "ignored");
|
|
476
|
+
return "ignored";
|
|
477
|
+
}
|
|
450
478
|
if (notice.kind === "sentinel-start")
|
|
451
479
|
return answerStart(notice, answer);
|
|
452
480
|
const task = noticeTask(notice, answer);
|
|
@@ -589,8 +617,8 @@ export function createChats(options) {
|
|
|
589
617
|
/**
|
|
590
618
|
* A pulse ended, with the sentinel on: the chat the person looks at says so - its agent told of one that landed, to
|
|
591
619
|
* tell the person it landed and what the sentinel does with it, and nothing more; of one that went wrong, not, the
|
|
592
|
-
* sentinel's notice being what it hears of it - and the sentinel is
|
|
593
|
-
*
|
|
620
|
+
* sentinel's notice being what it hears of it - and the sentinel is given it, opened in the background when it is not
|
|
621
|
+
* open.
|
|
594
622
|
*/
|
|
595
623
|
function sentinelFinished(job) {
|
|
596
624
|
const asked = chats.find((chat) => chat.id === job.chat && chat.role !== SENTINEL);
|
|
@@ -606,7 +634,7 @@ export function createChats(options) {
|
|
|
606
634
|
}
|
|
607
635
|
}
|
|
608
636
|
changed();
|
|
609
|
-
|
|
637
|
+
toSentinel(async () => {
|
|
610
638
|
let sentinel = sentinelChat();
|
|
611
639
|
if (sentinel === undefined) {
|
|
612
640
|
try {
|
|
@@ -622,23 +650,137 @@ export function createChats(options) {
|
|
|
622
650
|
}
|
|
623
651
|
if (sentinel?.instance === undefined)
|
|
624
652
|
return;
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
653
|
+
await takePulse(sentinel, job);
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
/** The sentinel's work, after what it was given before. */
|
|
657
|
+
function toSentinel(work) {
|
|
658
|
+
sentinelWork = sentinelWork.then(work).catch((error) => {
|
|
659
|
+
core.notify(`The sentinel: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
/** Whether the sentinel is at work: on a pulse - its turn starts as it is told of one - or on what the person said to it. */
|
|
663
|
+
const sentinelBusy = (sentinel) => sentinel.session?.isStreaming === true;
|
|
664
|
+
/**
|
|
665
|
+
* A finished pulse, for the sentinel: checked now when it is free. Busy, a newer pulse of a module replaces a check of
|
|
666
|
+
* the module's last pulse that went well - and a pulse of it that went well and waits - and one that went wrong is
|
|
667
|
+
* looked into to its end; the rest wait their turn.
|
|
668
|
+
*/
|
|
669
|
+
function takePulse(sentinel, job) {
|
|
670
|
+
if (!sentinelBusy(sentinel))
|
|
671
|
+
return check(sentinel, job);
|
|
672
|
+
const sameModule = (other) => job.module !== undefined && other.module === job.module;
|
|
673
|
+
if (checking !== undefined && !checking.wrong && sameModule(checking.job))
|
|
674
|
+
return check(sentinel, job, checking.job);
|
|
675
|
+
const older = waiting.findIndex((other) => sameModule(other) && !wentWrong(other));
|
|
676
|
+
if (older !== -1)
|
|
677
|
+
waiting.splice(older, 1, job);
|
|
678
|
+
else
|
|
679
|
+
waiting.push(job);
|
|
680
|
+
}
|
|
681
|
+
/** The sentinel free again: the next pulse that waits, checked. */
|
|
682
|
+
function nextPulse(sentinel) {
|
|
683
|
+
if (!chats.includes(sentinel) || sentinelBusy(sentinel))
|
|
684
|
+
return;
|
|
685
|
+
const job = waiting.shift();
|
|
686
|
+
if (job !== undefined)
|
|
687
|
+
return check(sentinel, job);
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* The sentinel on a pulse, from scratch: a conversation of its own - its prompt, the project's guidance and this
|
|
691
|
+
* pulse's brief, nothing of the pulses before it - in place of the one it had, stopped when it was on `replacing`.
|
|
692
|
+
*/
|
|
693
|
+
async function check(sentinel, job, replacing) {
|
|
694
|
+
checking = { job, wrong: wentWrong(job) };
|
|
695
|
+
const module = job.module;
|
|
696
|
+
inChat(sentinel.scope, () => record({ kind: "sentinel.checking", ...(module !== undefined ? { module } : {}), ...(replacing !== undefined ? { replaced: replacing.id } : {}) }, { project: cwd, job: job.id }));
|
|
697
|
+
const session = sentinel.session;
|
|
698
|
+
if (session !== undefined && (session.messages.length > 0 || session.isStreaming))
|
|
699
|
+
await freshConversation(sentinel);
|
|
700
|
+
if (sentinel.id !== shownId)
|
|
701
|
+
sentinel.unread = true;
|
|
702
|
+
const asked = chats.find((chat) => chat.id === job.chat && chat.role !== SENTINEL && chat.instance !== undefined);
|
|
703
|
+
const where = personFacing(asked);
|
|
704
|
+
try {
|
|
705
|
+
await sentinel.instance.brief(job, {
|
|
706
|
+
...(asked !== undefined ? { asked: titleFor(asked) } : {}),
|
|
707
|
+
...(where !== undefined ? { focused: titleFor(where) } : {}),
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
core.notify(`The sentinel could not be told of ${job.id}: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
712
|
+
checking = undefined;
|
|
713
|
+
}
|
|
714
|
+
changed();
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* The sentinel's chat given a new session, under the same id: what it does now stopped, and what it said let go - its
|
|
718
|
+
* saved conversation deleted outside debug mode. The model and thinking it had carry over: what the person picked in
|
|
719
|
+
* it stands. Shown in the terminal, the screen is drawn from the new one. A new session that cannot be made leaves it
|
|
720
|
+
* the one it had.
|
|
721
|
+
*/
|
|
722
|
+
async function freshConversation(chat) {
|
|
723
|
+
const old = { session: chat.session, services: chat.services, instance: chat.instance };
|
|
724
|
+
chat.unsubscribe?.();
|
|
725
|
+
chat.unsubscribe = undefined;
|
|
726
|
+
try {
|
|
727
|
+
await old.session.abort();
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
/* stopped already */
|
|
731
|
+
}
|
|
732
|
+
chat.working = false;
|
|
733
|
+
const model = old.session.model;
|
|
734
|
+
const thinking = old.session.thinkingLevel;
|
|
735
|
+
// Made as a chat is opened, one at a time: its extension built while it is the one attaching.
|
|
736
|
+
const made = opening.then(async () => {
|
|
737
|
+
attaching = chat;
|
|
630
738
|
try {
|
|
631
|
-
await
|
|
632
|
-
...(asked !== undefined ? { asked: titleFor(asked) } : {}),
|
|
633
|
-
...(where !== undefined ? { focused: titleFor(where) } : {}),
|
|
634
|
-
replace,
|
|
635
|
-
});
|
|
739
|
+
await driver.create(chat, undefined);
|
|
636
740
|
}
|
|
637
|
-
|
|
638
|
-
|
|
741
|
+
finally {
|
|
742
|
+
attaching = undefined;
|
|
639
743
|
}
|
|
640
|
-
|
|
641
|
-
|
|
744
|
+
});
|
|
745
|
+
opening = made.catch(() => undefined);
|
|
746
|
+
try {
|
|
747
|
+
await made;
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
Object.assign(chat, old);
|
|
751
|
+
follow(chat);
|
|
752
|
+
core.notify(`The sentinel goes on in the conversation it had - a new one could not be started: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
const fresh = chat.session;
|
|
756
|
+
try {
|
|
757
|
+
if (model !== undefined && fresh.model !== model)
|
|
758
|
+
await fresh.setModel(model);
|
|
759
|
+
if (thinking !== undefined)
|
|
760
|
+
fresh.setThinkingLevel(thinking);
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
/* the model its session started on, then */
|
|
764
|
+
}
|
|
765
|
+
follow(chat);
|
|
766
|
+
if (shownId === chat.id)
|
|
767
|
+
await driver?.show?.(chat, undefined);
|
|
768
|
+
try {
|
|
769
|
+
old.session.dispose();
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
/* let go already */
|
|
773
|
+
}
|
|
774
|
+
if (old.session.sessionFile !== undefined && old.session.sessionFile !== fresh.sessionFile)
|
|
775
|
+
forgetSessionFile(old.session.sessionFile, { keep: debugMode(cwd) });
|
|
776
|
+
try {
|
|
777
|
+
host.onChatSession?.(chat.id, fresh);
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
/* a host that throws still has the chat's new session in session(id) */
|
|
781
|
+
}
|
|
782
|
+
persist();
|
|
783
|
+
changed();
|
|
642
784
|
}
|
|
643
785
|
return core;
|
|
644
786
|
}
|
package/dist/extension.js
CHANGED
|
@@ -65,7 +65,7 @@ import { supersededByTurri } from "./chat-skills.js";
|
|
|
65
65
|
import { conversationIdentity } from "./identity.js";
|
|
66
66
|
import { setSurface, surfaceOf } from "./surface.js";
|
|
67
67
|
import { createChats, scopedToChat } from "./chats.js";
|
|
68
|
-
import { landedNote, pulseEndsBlock, SENTINEL, SENTINEL_TOOLS, sentinelBrief, sentinelPrompt, sentinelSetting, sentinelStartNotice, specConflictNotice } from "./sentinel.js";
|
|
68
|
+
import { ignoredGuidance, keepGuidance, landedNote, pulseEndsBlock, SENTINEL, SENTINEL_TOOLS, sentinelBrief, sentinelPrompt, sentinelSetting, sentinelStartNotice, specConflictNotice } from "./sentinel.js";
|
|
69
69
|
import { noticeDialogs, registerSentinel } from "./sentinel-chat.js";
|
|
70
70
|
import { currentChat } from "./chat-scope.js";
|
|
71
71
|
import { registerChats } from "./chat-commands.js";
|
|
@@ -327,6 +327,7 @@ export function createTurriCore(host = {}, options = {}) {
|
|
|
327
327
|
sentinel: () => sentinelSetting(cwd),
|
|
328
328
|
sentinelStart: (job) => sentinelStartNotice(job, cwd),
|
|
329
329
|
setSentinel: (on) => void writeSetting(cwd, "sentinel", on),
|
|
330
|
+
ignored: (notice) => keepGuidance(cwd, ignoredGuidance(notice)),
|
|
330
331
|
...(options.persist !== undefined ? { persist: options.persist } : {}),
|
|
331
332
|
...(options.dialogs !== undefined ? { dialogs: options.dialogs } : {}),
|
|
332
333
|
});
|
|
@@ -441,11 +442,10 @@ function turriExtension(api, session, link) {
|
|
|
441
442
|
details: { jobId: job.id, status: job.status },
|
|
442
443
|
}, { triggerTurn: true, deliverAs: "followUp" });
|
|
443
444
|
};
|
|
444
|
-
// The sentinel's: what a pulse that ended is, told to its agent
|
|
445
|
+
// The sentinel's: what a pulse that ended is, told to its agent in a conversation of its own for the pulse. The hidden
|
|
446
|
+
// message is the conversation's first entry - a host splits the sentinel's work by pulse on it - so the pulse's end is
|
|
447
|
+
// not drawn above it here: the person's chat shows it.
|
|
445
448
|
const brief = async (job, context) => {
|
|
446
|
-
if (context.replace)
|
|
447
|
-
await link.session()?.abort();
|
|
448
|
-
announceJob(pi, job);
|
|
449
449
|
const config = readProjectConfig(cwd);
|
|
450
450
|
const content = sentinelBrief(job, {
|
|
451
451
|
project: cwd,
|
|
@@ -455,8 +455,7 @@ function turriExtension(api, session, link) {
|
|
|
455
455
|
releaseAfterPulse: config.releaseAfterPulse === true,
|
|
456
456
|
reaction: JOB_FINISHED_REACTION,
|
|
457
457
|
});
|
|
458
|
-
|
|
459
|
-
void pi.sendMessage({ customType: "turri_sentinel_pulse", content, display: false, details: { jobId: job.id, status: job.status } }, { triggerTurn: true, ...(busy ? { deliverAs: "followUp" } : {}) });
|
|
458
|
+
void pi.sendMessage({ customType: "turri_sentinel_pulse", content, display: false, details: { jobId: job.id, status: job.status } }, { triggerTurn: true });
|
|
460
459
|
};
|
|
461
460
|
// A pulse ended, with the sentinel on: said in the chat. One that landed is told to its agent too, to tell the person
|
|
462
461
|
// it landed and what the sentinel does with it - and nothing more: the pulse is the sentinel's. ***plain that
|
package/dist/host-session.d.ts
CHANGED
|
@@ -68,9 +68,11 @@ export interface Turri {
|
|
|
68
68
|
notices(): Notice[];
|
|
69
69
|
/**
|
|
70
70
|
* A notice answered: one of its actions (by index or label) - the chat it is in started on it - the person's own words
|
|
71
|
-
* about it, or
|
|
71
|
+
* about it, dismissed, or - `ignorable: true` - ignored for good: left as the build has it, the sentinel told not to
|
|
72
|
+
* raise it again. `refused` for `ignore` on one that is not ignorable, `over` when it was answered or superseded
|
|
73
|
+
* already, `missing` for one never put.
|
|
72
74
|
*/
|
|
73
|
-
answer(id: string, answer: NoticeAnswer): Promise<"done" | "dismissed" | "missing" | "over">;
|
|
75
|
+
answer(id: string, answer: NoticeAnswer): Promise<"done" | "dismissed" | "ignored" | "refused" | "missing" | "over">;
|
|
74
76
|
/** Told when a chat is opened, closed, renamed, or starts or stops working. */
|
|
75
77
|
onChats(listener: (chats: ChatInfo[]) => void): () => void;
|
|
76
78
|
/** Every chat let go, and what activate started stopped. */
|
package/dist/notice.d.ts
CHANGED
|
@@ -29,8 +29,14 @@ export interface NoticeInput {
|
|
|
29
29
|
job?: string;
|
|
30
30
|
/** Its number among its module's pulses. */
|
|
31
31
|
pulse?: number;
|
|
32
|
+
/**
|
|
33
|
+
* The person may leave it as the build has it, for good (`{ ignore: true }`): a gap in the ***plain the build settled
|
|
34
|
+
* with an assumption - never something broken, a pulse that went wrong, a release that did not come up, nor the
|
|
35
|
+
* question whether to start the sentinel.
|
|
36
|
+
*/
|
|
37
|
+
ignorable?: boolean;
|
|
32
38
|
}
|
|
33
|
-
export type NoticeState = "open" | "accepted" | "steered" | "dismissed" | "superseded";
|
|
39
|
+
export type NoticeState = "open" | "accepted" | "steered" | "dismissed" | "ignored" | "superseded";
|
|
34
40
|
export interface Notice extends NoticeInput {
|
|
35
41
|
id: string;
|
|
36
42
|
/** The chat it is put in: the one the person looks at, never the sentinel's. */
|
|
@@ -46,19 +52,29 @@ export declare const SENTINEL_START: {
|
|
|
46
52
|
readonly never: "Turn it off for this project";
|
|
47
53
|
};
|
|
48
54
|
export type SentinelStartChoice = keyof typeof SENTINEL_START;
|
|
49
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* How the person answered a notice: one of its actions (by index or label), their own words, not at all this once
|
|
57
|
+
* (`dismiss`) - or, on an ignorable one, not at all and not again (`ignore`): left as the build has it.
|
|
58
|
+
*/
|
|
50
59
|
export type NoticeAnswer = {
|
|
51
60
|
action: number | string;
|
|
52
61
|
} | {
|
|
53
62
|
steer: string;
|
|
54
63
|
} | {
|
|
55
64
|
dismiss: true;
|
|
65
|
+
} | {
|
|
66
|
+
ignore: true;
|
|
56
67
|
};
|
|
57
68
|
export declare function newNotice(input: NoticeInput, chat: string): Notice;
|
|
58
69
|
/** The choice an answer to a `sentinel-start` notice makes: dismissed, or answered in the person's own words, it is not now. */
|
|
59
70
|
export declare function sentinelStartChoice(notice: Notice, answer: NoticeAnswer): SentinelStartChoice;
|
|
60
71
|
/** What the focused chat's agent is asked to do once the person answered a notice - undefined when they dismissed it. */
|
|
61
72
|
export declare function noticeTask(notice: Notice, answer: NoticeAnswer): string | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* What the sentinel keeps of a notice the person ignored, in its guidance: what was left as it is - its module, the gap,
|
|
75
|
+
* what the build does - and why, so a fresh conversation does not raise it again.
|
|
76
|
+
*/
|
|
77
|
+
export declare function ignoredGuidance(notice: Notice): string;
|
|
62
78
|
/** A notice in words: its summary, its evidence and what can be done - for the agent, the log and a chat without a card. */
|
|
63
79
|
export declare function describeNotice(notice: NoticeInput): string;
|
|
64
80
|
/** Whether a pulse went wrong: failed, stopped, or ran to its end with what it built not passing. */
|
package/dist/notice.js
CHANGED
|
@@ -24,7 +24,7 @@ export function sentinelStartChoice(notice, answer) {
|
|
|
24
24
|
}
|
|
25
25
|
/** What the focused chat's agent is asked to do once the person answered a notice - undefined when they dismissed it. */
|
|
26
26
|
export function noticeTask(notice, answer) {
|
|
27
|
-
if ("dismiss" in answer)
|
|
27
|
+
if ("dismiss" in answer || "ignore" in answer)
|
|
28
28
|
return undefined;
|
|
29
29
|
const head = [
|
|
30
30
|
`The sentinel put this to the person, about ${notice.module !== undefined ? `${notice.module}${notice.pulse !== undefined ? ` pulse ${notice.pulse}` : ""}` : "the work"}:`,
|
|
@@ -38,6 +38,15 @@ export function noticeTask(notice, answer) {
|
|
|
38
38
|
return undefined;
|
|
39
39
|
return [...head, `The person chose: ${action.label}. Do it now:`, action.task.trim()].join("\n");
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* What the sentinel keeps of a notice the person ignored, in its guidance: what was left as it is - its module, the gap,
|
|
43
|
+
* what the build does - and why, so a fresh conversation does not raise it again.
|
|
44
|
+
*/
|
|
45
|
+
export function ignoredGuidance(notice) {
|
|
46
|
+
const about = notice.module !== undefined ? ` in ${notice.module}` : "";
|
|
47
|
+
const shows = notice.evidence.length > 0 ? ` (${notice.evidence.join("; ")})` : "";
|
|
48
|
+
return `The person chose to leave this${about} as the build has it - do not raise it again unless the build no longer does what they accepted: ${notice.summary}${shows}`;
|
|
49
|
+
}
|
|
41
50
|
/** A notice in words: its summary, its evidence and what can be done - for the agent, the log and a chat without a card. */
|
|
42
51
|
export function describeNotice(notice) {
|
|
43
52
|
return [
|
package/dist/sentinel-chat.js
CHANGED
|
@@ -12,6 +12,7 @@ const NOTICE_ENTRY = "turri_notice";
|
|
|
12
12
|
const NOTICE_OVER_ENTRY = "turri_notice_over";
|
|
13
13
|
/** The sentinel done with a pulse, in the chat the person is in. */
|
|
14
14
|
const CONCLUDED_ENTRY = "turri_sentinel_concluded";
|
|
15
|
+
const IGNORE = "Ignore - leave it as the build has it";
|
|
15
16
|
const STEER = "Say what to do instead…";
|
|
16
17
|
const DISMISS = "Dismiss";
|
|
17
18
|
/** The sentinel's tools and model, built into its chat. */
|
|
@@ -99,7 +100,15 @@ export function noticeDialogs(pi, session, link, context) {
|
|
|
99
100
|
const data = entry.data;
|
|
100
101
|
if (data === undefined)
|
|
101
102
|
return undefined;
|
|
102
|
-
const said = data.state === "superseded"
|
|
103
|
+
const said = data.state === "superseded"
|
|
104
|
+
? "superseded by a newer notice"
|
|
105
|
+
: data.state === "dismissed"
|
|
106
|
+
? "dismissed"
|
|
107
|
+
: data.state === "ignored"
|
|
108
|
+
? "ignored - left as the build has it, not raised again"
|
|
109
|
+
: data.state === "steered"
|
|
110
|
+
? "answered in your words"
|
|
111
|
+
: "accepted";
|
|
103
112
|
const box = new Box(1, 0);
|
|
104
113
|
box.addChild(new Text(theme.fg("muted", ` ${said}: ${data.summary}`), 0, 0));
|
|
105
114
|
return box;
|
|
@@ -123,7 +132,12 @@ export function noticeDialogs(pi, session, link, context) {
|
|
|
123
132
|
try {
|
|
124
133
|
// Whether to start the sentinel is turri's question, its answers its own: no words of the person's to act on.
|
|
125
134
|
const starting = notice.kind === "sentinel-start";
|
|
126
|
-
const options = [
|
|
135
|
+
const options = [
|
|
136
|
+
...notice.actions.map((action, index) => `${index + 1}. ${action.label}`),
|
|
137
|
+
...(notice.ignorable === true ? [IGNORE] : []),
|
|
138
|
+
...(starting ? [] : [STEER]),
|
|
139
|
+
DISMISS,
|
|
140
|
+
];
|
|
127
141
|
const picked = await ctx.ui.select(starting ? startTitle(notice, session.chats().length) : `sentinel: ${notice.summary}`, options, { signal: controller.signal });
|
|
128
142
|
if (controller.signal.aborted || notice.state !== "open")
|
|
129
143
|
return;
|
|
@@ -137,6 +151,10 @@ export function noticeDialogs(pi, session, link, context) {
|
|
|
137
151
|
session.answer(notice.id, { steer: words });
|
|
138
152
|
return;
|
|
139
153
|
}
|
|
154
|
+
if (picked === IGNORE) {
|
|
155
|
+
session.answer(notice.id, { ignore: true });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
140
158
|
const index = options.indexOf(picked ?? DISMISS);
|
|
141
159
|
session.answer(notice.id, index >= 0 && index < notice.actions.length ? { action: index } : { dismiss: true });
|
|
142
160
|
}
|
package/dist/sentinel.js
CHANGED
|
@@ -137,7 +137,8 @@ export function sentinelPrompt(cwd, options = {}) {
|
|
|
137
137
|
return [
|
|
138
138
|
"<sentinel>",
|
|
139
139
|
"You are turri's sentinel: a chat of turri's own, beside the chats the person talks to while they build their software",
|
|
140
|
-
"with turri. Every pulse that finishes comes to you,
|
|
140
|
+
"with turri. Every pulse that finishes comes to you, each in a conversation of its own: this one is for the pulse in",
|
|
141
|
+
"the message turri writes first - what the pulse was, how it ended,",
|
|
141
142
|
...(surface === ""
|
|
142
143
|
? ["its ***plain diff, its log, and what was released. The person does not watch you: they are in another chat, and", "hear from you only through notify_person."]
|
|
143
144
|
: [
|
|
@@ -145,7 +146,7 @@ export function sentinelPrompt(cwd, options = {}) {
|
|
|
145
146
|
"they talk to you in another chat, and hear what you find only through notify_person.",
|
|
146
147
|
]),
|
|
147
148
|
"",
|
|
148
|
-
"For
|
|
149
|
+
"For the pulse:",
|
|
149
150
|
"1. Check how it went, from what turri tells you - and, when that is not enough, its log, the ledger (ledger, pulses),",
|
|
150
151
|
" diagnose, and the code and the ***plain, which you read.",
|
|
151
152
|
"2. When it was released (the message says so - release-after-pulse is on), test what was released: it is running",
|
|
@@ -156,17 +157,21 @@ export function sentinelPrompt(cwd, options = {}) {
|
|
|
156
157
|
" and what the person can do about it, each a task the agent of their chat can carry out once they accept it - a",
|
|
157
158
|
" change to the ***plain, a setting, a credential to provide, a pulse to regenerate again. Be exact: the task is",
|
|
158
159
|
" all that agent is given.",
|
|
160
|
+
" When it is a gap in the ***plain the build settled with an assumption - something the person may reasonably leave",
|
|
161
|
+
" as it is - say so with ignorable: the gap in the summary, what the build does in the evidence. Never for",
|
|
162
|
+
" something broken, a pulse that went wrong, or a release that did not come up.",
|
|
159
163
|
"4. When all is well, notify no one.",
|
|
160
|
-
"5. Last, call conclude once for
|
|
164
|
+
"5. Last, call conclude once for it - holds or wrong, and in a line or two what you checked and what you found:",
|
|
161
165
|
" the chat the person is in is told, whether all is well or not. Then say the same here, in a line or two.",
|
|
162
166
|
"",
|
|
163
167
|
"You change nothing. Your file tools read the whole project and write nothing; your shell runs with the project",
|
|
164
168
|
"read only. You never regenerate, steer, release or change ***plain yourself, and never ask the person questions -",
|
|
165
|
-
"a notice is how you reach them.
|
|
166
|
-
"that went well: leave the older one and check the newer.",
|
|
169
|
+
"a notice is how you reach them.",
|
|
167
170
|
"",
|
|
168
171
|
"The person can talk to you here. When they tell you how to check - what to test, what to leave alone, where things",
|
|
169
|
-
"run - keep it with keep_guidance, in their words, so it holds from now on
|
|
172
|
+
"run - keep it with keep_guidance, in their words, so it holds from now on: this conversation ends with its pulse,",
|
|
173
|
+
"and the next pulse's starts afresh, with only what you kept. What the person chose to leave as the build has it is",
|
|
174
|
+
"in your guidance too: do not raise it again, unless the build no longer does what they accepted.",
|
|
170
175
|
...(surface !== ""
|
|
171
176
|
? [
|
|
172
177
|
"",
|
|
@@ -265,6 +270,11 @@ export function notifyTool(notify) {
|
|
|
265
270
|
}), { description: "What can be done, the best first" }),
|
|
266
271
|
module: Type.Optional(Type.String({ description: "The module it is about" })),
|
|
267
272
|
pulse: Type.Optional(Type.String({ description: "The pulse's id" })),
|
|
273
|
+
ignorable: Type.Optional(Type.Boolean({
|
|
274
|
+
description: "true when the person may reasonably leave it as the build has it, for good: chiefly a gap in the ***plain the build settled with an " +
|
|
275
|
+
"assumption - then say the gap in the summary and what the build does in the evidence (the ***plain does not say what happens to " +
|
|
276
|
+
"passwords over 72 bytes; the build refuses them with 422). Never for something broken, a pulse that went wrong, or a release that did not come up.",
|
|
277
|
+
})),
|
|
268
278
|
}),
|
|
269
279
|
async execute(_id, params) {
|
|
270
280
|
const notice = notify({
|
|
@@ -272,6 +282,7 @@ export function notifyTool(notify) {
|
|
|
272
282
|
summary: params.summary,
|
|
273
283
|
evidence: params.evidence,
|
|
274
284
|
actions: params.actions,
|
|
285
|
+
...(params.ignorable === true ? { ignorable: true } : {}),
|
|
275
286
|
...(params.module !== undefined ? { module: params.module } : {}),
|
|
276
287
|
...(params.pulse !== undefined ? { job: params.pulse } : {}),
|
|
277
288
|
});
|
package/docs/chats.md
CHANGED
|
@@ -45,7 +45,7 @@ On macOS, `alt` is Option (⌥), and reaches turri only when the terminal sends
|
|
|
45
45
|
- **The side panel** (`ctrl+h`) is the session's, not a chat's: docked beside the chat or over it, it stays as another chat is shown, and opening it from any chat finds it as it was left.
|
|
46
46
|
- **A long command** - `/regenerate`, planning a module from scratch - does its work in the background once it is under way: the chat, and switching to another, stay free while the planner plans.
|
|
47
47
|
- **`ctrl+c`** reaches the chat shown and nothing else: another chat's turn and skill go on, and so do the pulses - `/halt` stops one. A skill stopped keeps its run, for the model to continue with `resume`. At a question - the chat's `ask_user`, a skill's interview - `ctrl+c` declines it, and the asker is told so and goes on - each question it is pressed at, however many in a row. A skill is stopped by `ctrl+c` with no question open. A second press straight after one that stopped something does nothing, so pressing twice to be sure does not leave turri; once the chat is doing nothing, the next one does - with what every other chat is doing. A host has the same as `interrupt(id)`, and `decline(id)` for a question ([hosting](hosting.md)).
|
|
48
|
-
- **The sentinel** is a chat in the strip like the others, `sentinel`, and never shown by itself: it closes like any chat, and the next pulse that finishes opens it again. With it on, you open up to eight chats - the ninth slot is its. See [the sentinel](sentinel.md).
|
|
48
|
+
- **The sentinel** is a chat in the strip like the others, `sentinel`, and never shown by itself: it closes like any chat, and the next pulse that finishes opens it again. Each pulse it checks in a fresh conversation, replacing the one in its tab. With it on, you open up to eight chats - the ninth slot is its. See [the sentinel](sentinel.md).
|
|
49
49
|
- **The last chat** of yours is not closed: `/quit` leaves turri. Leaving turri leaves the terminal as it was: the conversation is not written out to it, nor a line on how to resume - the command run is all that stays. `turri --continue` opens the chats again.
|
|
50
50
|
- **`/fork`** and **`/reload`** work on the chat shown, as [pi](https://pi.dev)'s do: the chat's history forked, or turri's extension rebuilt in it.
|
|
51
51
|
- **A chat shown mid-turn** draws its reply as far as it is written, and follows it from there.
|
package/docs/hosting.md
CHANGED
|
@@ -84,8 +84,9 @@ const turri = await createTurri({
|
|
|
84
84
|
extensions: [browser, terminal], // the host's own extensions, built into every chat after turri's
|
|
85
85
|
restore: true, // reopen the last session's chats here, as `turri --continue` does
|
|
86
86
|
onNotice: (notice) => {}, // the sentinel puts something to the person: draw it in notice.chat
|
|
87
|
-
onNoticeOver: (notice) => {}, // ...answered, dismissed or superseded: take its card down
|
|
87
|
+
onNoticeOver: (notice) => {}, // ...answered, dismissed, ignored or superseded: take its card down
|
|
88
88
|
onConcluded: (conclusion) => {}, // the sentinel is done with a pulse: holds or wrong, told to conclusion.chat
|
|
89
|
+
onChatSession: (chat, session) => {}, // a chat's session replaced, same id: the sentinel's, fresh for each pulse - follow this one
|
|
89
90
|
sentinel: {
|
|
90
91
|
tools: [browserTool], // the sentinel's own tools, beside turri's: what it tests a release with
|
|
91
92
|
surface: "...", // where the person watches it use them, in the host's words
|
|
@@ -100,7 +101,7 @@ await turri.interrupt(chat.id); // ctrl+c: "stopped" what it was doing, e
|
|
|
100
101
|
await turri.decline(chat.id); // ctrl+c at a question: "declined", the turn going on - "none", "missing"
|
|
101
102
|
turri.chats(); // [{ id, title, status: "idle" | "working" | "question" | "unread", shown, role? }]
|
|
102
103
|
turri.notices(); // the notices open now
|
|
103
|
-
await turri.answer(notice.id, { action: 0 }); // or { steer: "…" }, { dismiss: true }: "done", "dismissed", "over", "missing"
|
|
104
|
+
await turri.answer(notice.id, { action: 0 }); // or { steer: "…" }, { dismiss: true }, { ignore: true }: "done", "dismissed", "ignored", "refused", "over", "missing"
|
|
104
105
|
turri.onChats((chats) => {}); // a chat opened, closed, renamed, working or done
|
|
105
106
|
await turri.closeChat(chat.id); // its turn stopped, its conversation deleted - its pulses run on
|
|
106
107
|
turri.jobs; // the session's job registry: every pulse any chat started
|
|
@@ -122,7 +123,9 @@ await turri.dispose(); // every chat let go, what activate start
|
|
|
122
123
|
|
|
123
124
|
The [sentinel](sentinel.md) is one of the host's chats: turri opens it with the first pulse that finishes, through the host's own `create` like any chat - `onChats` hears of it, `role: "sentinel"` - and never moves `focus` to it. It closes like any chat, and the next pulse that finishes opens it again; with it on, `openChat` opens up to eight.
|
|
124
125
|
|
|
125
|
-
- **
|
|
126
|
+
- **A conversation a pulse.** The sentinel checks each pulse from scratch, in a conversation of its own - one pulse at a time, the others waiting their turn. The chat keeps its id; its session is replaced. As it starts on a pulse, the session it had is stopped and let go (`abort`, `dispose`), a new one is made in its place, and `onChatSession(chat, session)` comes with it - before the pulse is put to it - then `onChats`; `session(id)` returns the new one from then on. A host that follows the sentinel's chat subscribes to that session in place of the old - an `onChats` listener that follows only the ids it has not seen would hear nothing more of it. The first pulse goes to the session the chat was opened with: there is no `onChatSession` for it. Each conversation's first message is the hidden brief of its pulse - `customType: "turri_sentinel_pulse"`, `details.jobId` - so a host that splits the sentinel's work by pulse on it still can; the conversations before are not in the new session, and what the host showed of them is its own to keep.
|
|
127
|
+
|
|
128
|
+
- **Notices.** What it finds wrong comes as `onNotice(notice)`: `{ id, chat, severity: "error" | "warning" | "info", summary, evidence: string[], actions: [{ label, task }], module?, pulse?, job?, ignorable?, state: "open" }`. Draw it in the chat `chat` names - the one `focus` last named, or, while that is the sentinel, the chat that asked for the pulse. It is drawn there as a card in the transcript too. A host given `onNotice` answers it itself - the terminal's dialog is not opened - with `answer(id, …)`: `{ action: index | label }` starts that chat's agent on the action's task, `{ steer: text }` on the person's words, `{ dismiss: true }` on nothing, this once. A notice with `ignorable: true` - a gap in the \*\*\*plain the build settled with an assumption, which the person may leave as it is - takes `{ ignore: true }` too: offer it as its own button beside the actions. Nothing is done in the chat, the notice ends `state: "ignored"` (`"ignored"`), and turri writes it to the sentinel's guidance, so it is not raised again in this session or the next; on a notice without `ignorable`, `{ ignore: true }` is `"refused"` and the notice stays open. `onNoticeOver(notice)` comes as it is answered - and when a newer notice about its module supersedes it (`state: "superseded"`) - for the host to take the card down. `notices()` lists the open ones.
|
|
126
129
|
- **Starting it.** Under `sentinel: ask` - as it is unset - the person is asked before it first opens in the session: a notice with `kind: "sentinel-start"`, `severity: "info"`, in the chat `chat` names, its `summary` the question in one sentence, no `evidence`, and its actions the answers - `Start the sentinel`, `Start it, and don't ask again`, `Not this session`, `Turn it off for this project`. Draw it as a question rather than as something wrong. `answer(id, { action })` answers it, by index or label - `"done"` - and `{ dismiss: true }` is not this session; turri writes `sentinel:` itself for the two that choose for the project. Until it is answered, the pulses that finish are held: neither the sentinel nor the chat hears of them. A host that offers the setting too writes it as `/turri-config` does - `ask`, `true` or `false` (see `configSettings`).
|
|
127
130
|
- **Conclusions.** As the sentinel is done with each pulse - all well or not - `onConcluded({ job, verdict: "holds" | "wrong", summary, module, pulse, chat })` comes, and the chat `chat` names - the one `focus` last named, as for a notice - shows it and its agent says so in a line.
|
|
128
131
|
- **Its model.** The `sentinel` role in `models:`, else the chat's model - `models.chat`, else `harness-model` - with its thinking, set as its chat opens: not pi's default model, which may not be one the project's provider has. What the person then picks in its chat stands. See [its model](sentinel.md#its-model).
|
package/docs/sentinel.md
CHANGED
|
@@ -20,17 +20,18 @@ Nothing goes to the sentinel, or to your chat, until you answer: the pulses that
|
|
|
20
20
|
## What you see
|
|
21
21
|
|
|
22
22
|
- **In the chat you are in**, a pulse that finishes shows as it always has - `✓ app · regenerate pulse 3 finished after 4m 10s`, and for one that went wrong, where to see why. A pulse that landed is told to the chat's agent, which tells you in a line or two what landed and what the sentinel is doing with it - and again, in a line, when the sentinel is done with it - checking it, testing what was released - and does nothing else about it: it does not test, release or open it, nor do what the project's guidance says to do once a pulse finishes. The pulse is the sentinel's. A pulse that went wrong is not told to it: the sentinel looks into it, and you hear of it as a notice.
|
|
23
|
-
- **The sentinel** opens with the first pulse that finishes - once you said to start it, under `sentinel: ask` - in the background: it never takes the screen, and you stay in your chat. It is a tab in the strip like any other chat - `sentinel` - marked while it works and when it has something new. Switch to it to read what it checked.
|
|
23
|
+
- **The sentinel** opens with the first pulse that finishes - once you said to start it, under `sentinel: ask` - in the background: it never takes the screen, and you stay in your chat. It is a tab in the strip like any other chat - `sentinel` - marked while it works and when it has something new. Switch to it to read what it checked: it shows the pulse it is on, or the last one it checked - each pulse has a conversation of its own (below).
|
|
24
24
|
- **A notice**, when something needs you: a card in the chat you are in - what is wrong, what shows it, and what can be done, numbered - and, in the terminal, once that chat is not mid-turn, a dialog on it:
|
|
25
25
|
- `enter` on an action accepts it: the chat's agent is started on it - a change to the \*\*\*plain, a pulse regenerated again, a credential to provide, a setting to change;
|
|
26
|
+
- **Ignore - leave it as the build has it**, on a notice the sentinel marked ignorable: nothing is done, and it is not raised again - in this session or the next (below);
|
|
26
27
|
- **Say what to do instead…** - your own words, and the agent does that;
|
|
27
|
-
- **Dismiss**, or `esc`: nothing is done.
|
|
28
|
+
- **Dismiss**, or `esc`: nothing is done, this once - the next pulse may raise it again.
|
|
28
29
|
|
|
29
30
|
Nothing is done without your answer. A notice goes to the chat you are in; while you are in the sentinel's, to the chat that asked for the pulse. There is one open notice a module: a newer one about the same module supersedes the older one, whose call to action would be stale, and the card says so.
|
|
30
31
|
|
|
31
32
|
## What it does
|
|
32
33
|
|
|
33
|
-
For each pulse that finishes, turri writes the sentinel what it was and how it ended - its stages, rounds and failures as the [ledger](using.md#the-ledger) has them, its \*\*\*plain diff, its log, the chat that asked for it, the chat you are in, and what was released and where it runs. Then it:
|
|
34
|
+
Each pulse it checks from scratch, in a conversation of its own: its prompt, your guidance (`.turri/sentinel.md`, below) and what turri writes it of that pulse - nothing of the pulses before it. Its chat stays the same tab; the conversation in it is replaced as it starts on the next pulse, and the one before is let go - deleted, outside [debug mode](config.md). For each pulse that finishes, turri writes the sentinel what it was and how it ended - its stages, rounds and failures as the [ledger](using.md#the-ledger) has them, its \*\*\*plain diff, its log, the chat that asked for it, the chat you are in, and what was released and where it runs. Then it:
|
|
34
35
|
|
|
35
36
|
1. **checks the pulse** - from what it is told, and when that is not enough, from the pulse's log, the ledger (`ledger`, `pulses`), `diagnose`, the code and the \*\*\*plain;
|
|
36
37
|
2. **tests the release**, with `release-after-pulse` on: what was released is running where it is told, and it tries it - in the terminal with its shell (`curl`, the program itself, the logs), in a host also with what the host gives it, a browser say. How far it tests is its own judgement, from what the pulse changed and what the \*\*\*plain says it now does. With `release-after-pulse` off nothing is running, and it checks the pulse alone;
|
|
@@ -38,7 +39,11 @@ For each pulse that finishes, turri writes the sentinel what it was and how it e
|
|
|
38
39
|
4. **notifies no one** when all is well;
|
|
39
40
|
5. **concludes**, last, whether all is well or not: what it checked and what it found, in a line or two. The chat you are in shows it - `✓ sentinel · app · pulse 3 · checked, it holds` - and its agent tells you so in a line, and does nothing more; when something was wrong, that its notice says what.
|
|
40
41
|
|
|
41
|
-
A pulse that
|
|
42
|
+
It checks one pulse at a time. A pulse that finishes while it works waits its turn, and is checked once it is done - from scratch, as every pulse is. A pulse that went well is checked until a newer pulse of its module finishes: its check is stopped, and the newer one is checked instead, at once - and a newer pulse of a module takes the place of one of it that went well and waits. A pulse that went wrong is looked into to its end.
|
|
43
|
+
|
|
44
|
+
A notice may be **ignorable**: the sentinel marks one so when you may reasonably leave it as it is - chiefly a gap in the \*\*\*plain the build settled with an assumption: *the \*\*\*plain does not say what happens to passwords over 72 bytes; the build refuses them with 422*. Never something broken, a pulse that went wrong, a release that did not come up, nor the question whether to start the sentinel. Ignore one and it is left as the build has it, for good: nothing is done in your chat, and a line is written to the sentinel's guidance (`.turri/sentinel.md`, as `keep_guidance` writes) saying what was left as it is - its module, the gap, what the build does - and that you chose to. Every conversation after has it, and does not raise it again - unless the build no longer does what you accepted. To have it raised again, take the line out of `.turri/sentinel.md`.
|
|
45
|
+
|
|
46
|
+
Two functional specs that contradict each other stop a pulse in a way only you can settle: turri puts that notice itself, with an action that starts your chat on working it out with you.
|
|
42
47
|
|
|
43
48
|
## What it may do
|
|
44
49
|
|
|
@@ -46,22 +51,22 @@ Nothing that changes the project. Its file tools - `read`, `ls`, `grep`, `find`
|
|
|
46
51
|
|
|
47
52
|
## Talking to it
|
|
48
53
|
|
|
49
|
-
Switch to its chat and say how to check your software - *test the sign-up flow at a phone's width*, *ignore the styling*, *the API runs on port 8080*. What should hold from now on it keeps with `keep_guidance`, in `.turri/sentinel.md`, and it is in its prompt from then on - after it is closed and opened again too.
|
|
54
|
+
Switch to its chat and say how to check your software - *test the sign-up flow at a phone's width*, *ignore the styling*, *the API runs on port 8080*. What should hold from now on it keeps with `keep_guidance`, in `.turri/sentinel.md`, and it is in its prompt from then on - for every pulse after, and after it is closed and opened again too. Nothing else you say carries over: the conversation you talk to it in is the pulse's, and the next pulse starts a fresh one. A pulse that finishes while it answers you waits until it has.
|
|
50
55
|
|
|
51
56
|
## Closing it
|
|
52
57
|
|
|
53
|
-
It closes like any chat (`/chat close`, `alt+w`, a host's `closeChat`), and that does not turn it off: the next pulse that finishes opens it again, with a fresh conversation - and your guidance. `turri --continue` reopens it with the other chats,
|
|
58
|
+
It closes like any chat (`/chat close`, `alt+w`, a host's `closeChat`), and that does not turn it off: what it was checking is stopped, the pulses that waited for it are let go, and the next pulse that finishes opens it again, with a fresh conversation - and your guidance. `turri --continue` reopens it with the other chats, with the conversation of the last pulse it was on; the next pulse starts a fresh one. `sentinel: false` - in `config.turri.yaml`, or with `/sentinel off` or `/turri-config` - turns it off: from the next pulse that finishes, a pulse is told to the chat that asked for it, whose agent reacts to it, as without one.
|
|
54
59
|
|
|
55
60
|
It has a slot of its own: with it on, you open up to eight chats, and the ninth is the sentinel's, open or not.
|
|
56
61
|
|
|
57
62
|
## Its model
|
|
58
63
|
|
|
59
|
-
The `sentinel` role in `models:` - else the chat's model, `models.chat` else `harness-model`, with its thinking - see [models](config.md#models). A `sentinel` entry that gives only `thinking` runs on the chat's model, thinking as it says. It is set as the sentinel's chat opens, in the terminal and in a host alike - never the model its session started on, which in a host is [pi](https://pi.dev)'s default and may not be one the project's provider has. What the person then picks in its chat stands. Judging behaviour against \*\*\*plain, and driving a browser, want a capable model; point it at a cheaper one if it costs too much for every pulse.
|
|
64
|
+
The `sentinel` role in `models:` - else the chat's model, `models.chat` else `harness-model`, with its thinking - see [models](config.md#models). A `sentinel` entry that gives only `thinking` runs on the chat's model, thinking as it says. It is set as the sentinel's chat opens, in the terminal and in a host alike - never the model its session started on, which in a host is [pi](https://pi.dev)'s default and may not be one the project's provider has. What the person then picks in its chat stands, through each pulse's fresh conversation, until it is closed. Judging behaviour against \*\*\*plain, and driving a browser, want a capable model; point it at a cheaper one if it costs too much for every pulse.
|
|
60
65
|
|
|
61
66
|
## In a host
|
|
62
67
|
|
|
63
|
-
A host sees the sentinel as one of its chats - `role: "sentinel"` in `chats()` and `onChats` - and never has focus moved to it. Under `sentinel: ask`, the question before it first opens comes as a notice like the others, with `kind: "sentinel-start"`, its actions the four answers: `answer(id, { action })` answers it - by index or label - and `{ dismiss: true }` is not this session. Each notice comes as `onNotice(notice)` - `{ id, chat, module, pulse, job, severity, summary, evidence, actions, state }`, `chat` the one to draw it in - and `onNoticeOver(notice)` when it is answered or superseded; `notices()` lists those open. `answer(id, { action } | { steer } | { dismiss: true })` answers one, as the terminal's dialog does
|
|
68
|
+
A host sees the sentinel as one of its chats - `role: "sentinel"` in `chats()` and `onChats` - and never has focus moved to it. Each pulse's fresh conversation is a new pi session in the same chat, under the same id: `onChatSession(chat, session)` comes before the pulse is put to it, and `session(id)` returns the new one. Under `sentinel: ask`, the question before it first opens comes as a notice like the others, with `kind: "sentinel-start"`, its actions the four answers: `answer(id, { action })` answers it - by index or label - and `{ dismiss: true }` is not this session. Each notice comes as `onNotice(notice)` - `{ id, chat, module, pulse, job, severity, summary, evidence, actions, state }`, `chat` the one to draw it in - and `onNoticeOver(notice)` when it is answered or superseded; `notices()` lists those open. `answer(id, { action } | { steer } | { dismiss: true } | { ignore: true })` answers one, as the terminal's dialog does - `{ ignore: true }` only on a notice with `ignorable: true`, `"refused"` on any other; it ends `state: "ignored"`. `onConcluded({ job, verdict: "holds" | "wrong", summary, module, pulse, chat })` comes as it is done with a pulse, all well or not - told to the chat `chat` too. The sentinel's own tools - a browser - are given with `createTurri({ sentinel: { tools } })`, and it is told it has them. `sentinel: { surface }` says, in the host's words, where the person watches it use them: then it tries a release with them, where the person can watch, and keeps its shell for what they cannot show - logs, exit codes, files. Without it, the person does not watch the sentinel, and the host's tools are only to test with as well. See [running turri in a host](hosting.md#the-sentinel).
|
|
64
69
|
|
|
65
70
|
## In the ledger
|
|
66
71
|
|
|
67
|
-
`chat.opened` carries `role: "sentinel"` for its chat; `notice.put` and `notice.over` record each notice - the question before it opens among them - - its severity and module, and how it ended: `accepted`, `steered`, `dismissed`, `superseded` - and `sentinel.concluded` each pulse it is done with, `holds` or `wrong`.
|
|
72
|
+
`chat.opened` carries `role: "sentinel"` for its chat; `sentinel.checking` each pulse it starts a conversation for - `replaced`, the pulse of the same module whose check it stopped; `notice.put` and `notice.over` record each notice - the question before it opens among them - - its severity and module, and how it ended: `accepted`, `steered`, `dismissed`, `ignored`, `superseded` - and `sentinel.concluded` each pulse it is done with, `holds` or `wrong`.
|
package/docs/using.md
CHANGED
|
@@ -187,7 +187,7 @@ It is in `.turri/ledger/` in the project, a JSON line an event, one file a run -
|
|
|
187
187
|
| the healers | `healer.started`, `healer.ended` with what it found, `healer.failed` and why - the debugger, the diagnosis of a failed pulse |
|
|
188
188
|
| the history | `history.committed`, `history.failed`, `history.reset`, `history.rolledBack` |
|
|
189
189
|
| the environment | `environment.error` |
|
|
190
|
-
| chats and notices | `chat.opened` (`role: "sentinel"` for the [sentinel](sentinel.md)'s), `chat.closed`, `notice.put` (its severity and module), `notice.over` (accepted, steered, dismissed or superseded), `sentinel.concluded` (`holds` or `wrong`, a pulse the sentinel is done with) |
|
|
190
|
+
| chats and notices | `chat.opened` (`role: "sentinel"` for the [sentinel](sentinel.md)'s), `chat.closed`, `notice.put` (its severity and module), `notice.over` (accepted, steered, dismissed, ignored or superseded), `sentinel.checking` (a pulse the sentinel starts a conversation for - `replaced`, the one whose check it stopped), `sentinel.concluded` (`holds` or `wrong`, a pulse the sentinel is done with) |
|
|
191
191
|
|
|
192
192
|
Never in it: prompts, replies, thinking, tool arguments and results, file contents - the one body it keeps is each pulse's \*\*\*plain diff, so what an earlier session regenerated can be read back. What an event does carry - a stage, an error's first line - is masked as the logs are.
|
|
193
193
|
|