@turri-dev/cli 0.1.17 → 0.1.18
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/ask.d.ts +7 -2
- package/dist/chat-scope.d.ts +2 -0
- package/dist/chats.d.ts +2 -0
- package/dist/chats.js +10 -1
- package/dist/extension.js +54 -16
- package/dist/host-session.d.ts +12 -1
- package/dist/host-session.js +1 -0
- package/docs/chats.md +1 -1
- package/docs/hosting.md +5 -3
- package/package.json +1 -1
package/dist/ask.d.ts
CHANGED
|
@@ -36,8 +36,13 @@ export type AskUser = (request: AskUserRequest) => Promise<string>;
|
|
|
36
36
|
*/
|
|
37
37
|
export interface AskUserIO {
|
|
38
38
|
/** `at`: the option the cursor starts on - the one just ticked, in a question of several. */
|
|
39
|
-
select(title: string, options: string[], context?: string, at?: number): Promise<string | undefined>;
|
|
40
|
-
input(title: string, context?: string): Promise<string | undefined>;
|
|
39
|
+
select(title: string, options: string[], context?: string, at?: number, asked?: DialogAsked): Promise<string | undefined>;
|
|
40
|
+
input(title: string, context?: string, asked?: DialogAsked): Promise<string | undefined>;
|
|
41
|
+
}
|
|
42
|
+
/** What a host's dialog is told beside its question. */
|
|
43
|
+
export interface DialogAsked {
|
|
44
|
+
/** Aborted when the question is over without the dialog's answer - declined, or its turn stopped: the dialog is closed. */
|
|
45
|
+
signal: AbortSignal;
|
|
41
46
|
}
|
|
42
47
|
export declare const OTHER_OPTION = "Something else\u2026";
|
|
43
48
|
export declare const CHAT_OPTION = "Chat about this";
|
package/dist/chat-scope.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface ChatScope {
|
|
|
7
7
|
id: string;
|
|
8
8
|
/** The stack this chat chose with /stack or the stack tool - none: the project's config alone. */
|
|
9
9
|
stack: string | undefined;
|
|
10
|
+
/** The questions it asks now, each with what declines it: a host declines them with `Turri.decline`. */
|
|
11
|
+
readonly asking: Set<() => void>;
|
|
10
12
|
}
|
|
11
13
|
/** The chat this code runs for - undefined outside every chat: the command line, a test. */
|
|
12
14
|
export declare function currentChat(): ChatScope | undefined;
|
package/dist/chats.d.ts
CHANGED
|
@@ -113,6 +113,8 @@ export interface TurriCore {
|
|
|
113
113
|
chats(): ChatInfo[];
|
|
114
114
|
/** The chat's pi session. */
|
|
115
115
|
session(id: string): AgentSession | undefined;
|
|
116
|
+
/** The questions the chat asks now declined, as ctrl+c at one declines it - `none` when it asks nothing. */
|
|
117
|
+
decline(id: string): "declined" | "none" | "missing";
|
|
116
118
|
/** The chat shown now. */
|
|
117
119
|
shown(): string | undefined;
|
|
118
120
|
/** Open a chat - fresh, or the saved one `sessionFile` is - shown at once unless `hidden`. */
|
package/dist/chats.js
CHANGED
|
@@ -109,7 +109,7 @@ export function createChats(options) {
|
|
|
109
109
|
const id = randomUUID().slice(0, 8);
|
|
110
110
|
const chat = {
|
|
111
111
|
id,
|
|
112
|
-
scope: { id, stack: process.env.TURRI_STACK?.trim() || undefined },
|
|
112
|
+
scope: { id, stack: process.env.TURRI_STACK?.trim() || undefined, asking: new Set() },
|
|
113
113
|
...(title !== undefined ? { recordedTitle: title } : {}),
|
|
114
114
|
working: false,
|
|
115
115
|
unread: false,
|
|
@@ -155,6 +155,15 @@ export function createChats(options) {
|
|
|
155
155
|
},
|
|
156
156
|
chats: () => chats.map((chat) => ({ id: chat.id, title: titleFor(chat), status: statusOf(chat), shown: chat.id === shownId })),
|
|
157
157
|
session: (id) => chats.find((chat) => chat.id === id)?.session,
|
|
158
|
+
decline(id) {
|
|
159
|
+
const chat = chats.find((candidate) => candidate.id === id);
|
|
160
|
+
if (chat === undefined)
|
|
161
|
+
return "missing";
|
|
162
|
+
const asking = [...chat.scope.asking];
|
|
163
|
+
for (const decline of asking)
|
|
164
|
+
decline();
|
|
165
|
+
return asking.length > 0 ? "declined" : "none";
|
|
166
|
+
},
|
|
158
167
|
shown: () => shownId,
|
|
159
168
|
shownChat: () => chats.find((chat) => chat.id === shownId),
|
|
160
169
|
chatOfSession: (session) => chats.find((chat) => chat.session === session),
|
package/dist/extension.js
CHANGED
|
@@ -63,6 +63,7 @@ import { supersededByTurri } from "./chat-skills.js";
|
|
|
63
63
|
import { conversationIdentity } from "./identity.js";
|
|
64
64
|
import { setSurface, surfaceOf } from "./surface.js";
|
|
65
65
|
import { createChats, scopedToChat } from "./chats.js";
|
|
66
|
+
import { currentChat } from "./chat-scope.js";
|
|
66
67
|
import { registerChats } from "./chat-commands.js";
|
|
67
68
|
import { turriIdentity } from "./environment.js";
|
|
68
69
|
import { conformanceChooser } from "./regenerate/choose-conformance.js";
|
|
@@ -95,15 +96,9 @@ dialogs) {
|
|
|
95
96
|
if (dialogs === undefined && !ctx.hasUI) {
|
|
96
97
|
return "No user is available to answer. Decide yourself, and say in your report what you assumed.";
|
|
97
98
|
}
|
|
98
|
-
// Declined - ctrl+c at it, or the turn that asked it stopped: the dialog open is closed, no other is
|
|
99
|
-
// comes back as DECLINED.
|
|
100
|
-
const
|
|
101
|
-
const declining = { declined: turn?.aborted === true, close: undefined };
|
|
102
|
-
const decline = () => {
|
|
103
|
-
declining.declined = true;
|
|
104
|
-
declining.close?.();
|
|
105
|
-
};
|
|
106
|
-
turn?.addEventListener("abort", decline, { once: true });
|
|
99
|
+
// Declined - ctrl+c at it, the host's decline, or the turn that asked it stopped: the dialog open is closed, no other is
|
|
100
|
+
// opened, and it comes back as DECLINED.
|
|
101
|
+
const { declining, decline, done } = asking(ctx.signal);
|
|
107
102
|
// The question has the keys: ctrl+h is caught here, or it would never reach the shortcut, which only the chat's editor sees.
|
|
108
103
|
const unhook = dialogs === undefined ? hookPeekKey(ctx) : () => { };
|
|
109
104
|
if (dialogs === undefined)
|
|
@@ -119,19 +114,55 @@ dialogs) {
|
|
|
119
114
|
return answer;
|
|
120
115
|
}
|
|
121
116
|
finally {
|
|
122
|
-
|
|
117
|
+
done();
|
|
123
118
|
unhook();
|
|
124
119
|
if (dialogs === undefined)
|
|
125
120
|
ctx.ui.setWorkingMessage();
|
|
126
121
|
}
|
|
127
122
|
};
|
|
128
123
|
}
|
|
129
|
-
/**
|
|
124
|
+
/**
|
|
125
|
+
* A question being asked, until `done`: declined by `decline` - ctrl+c at it - by the host (`Turri.decline`, through the
|
|
126
|
+
* chat's `asking`) or by `turn` being stopped.
|
|
127
|
+
*/
|
|
128
|
+
function asking(turn) {
|
|
129
|
+
const declining = { declined: turn?.aborted === true, close: undefined };
|
|
130
|
+
const decline = () => {
|
|
131
|
+
declining.declined = true;
|
|
132
|
+
declining.close?.();
|
|
133
|
+
};
|
|
134
|
+
turn?.addEventListener("abort", decline, { once: true });
|
|
135
|
+
const open = currentChat()?.asking;
|
|
136
|
+
open?.add(decline);
|
|
137
|
+
return {
|
|
138
|
+
declining,
|
|
139
|
+
decline,
|
|
140
|
+
done: () => {
|
|
141
|
+
turn?.removeEventListener("abort", decline);
|
|
142
|
+
open?.delete(decline);
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* A host's dialogs, for a question that can be declined: the question over at once, its dialog told through its
|
|
148
|
+
* `signal` - the host's to close.
|
|
149
|
+
*/
|
|
130
150
|
function hostDialogs(dialogs, declining) {
|
|
131
|
-
const until = (asked) =>
|
|
151
|
+
const until = (asked) => {
|
|
152
|
+
if (declining.declined)
|
|
153
|
+
return Promise.resolve(undefined);
|
|
154
|
+
const over = new AbortController();
|
|
155
|
+
return Promise.race([
|
|
156
|
+
asked(over.signal),
|
|
157
|
+
new Promise((resolve) => (declining.close = () => {
|
|
158
|
+
over.abort();
|
|
159
|
+
resolve(undefined);
|
|
160
|
+
})),
|
|
161
|
+
]);
|
|
162
|
+
};
|
|
132
163
|
return {
|
|
133
|
-
select: (
|
|
134
|
-
input: (
|
|
164
|
+
select: (title, options, context, at) => until((signal) => dialogs.select(title, options, context, at, { signal })),
|
|
165
|
+
input: (title, context) => until((signal) => dialogs.input(title, context, { signal })),
|
|
135
166
|
};
|
|
136
167
|
}
|
|
137
168
|
/**
|
|
@@ -753,8 +784,15 @@ function turriExtension(api, session, link) {
|
|
|
753
784
|
escalate: async ({ question, critique }) => {
|
|
754
785
|
if (dialogs !== undefined) {
|
|
755
786
|
const through = "Let it through";
|
|
756
|
-
|
|
757
|
-
|
|
787
|
+
// Declined, as any question in the host's dialogs can be: the gate stands.
|
|
788
|
+
const asked = asking(signal);
|
|
789
|
+
try {
|
|
790
|
+
const answer = await hostDialogs(dialogs, asked.declining).select(`Review gate: ${skill.name} - ${question}`, [through, "Keep the gate"], critique);
|
|
791
|
+
return !asked.declining.declined && answer === through;
|
|
792
|
+
}
|
|
793
|
+
finally {
|
|
794
|
+
asked.done();
|
|
795
|
+
}
|
|
758
796
|
}
|
|
759
797
|
if (!ctx.hasUI) {
|
|
760
798
|
push(" no UI available for human review - gate stands");
|
package/dist/host-session.d.ts
CHANGED
|
@@ -16,7 +16,11 @@ export interface TurriOptions extends Omit<TurriHost, "dialogs"> {
|
|
|
16
16
|
sessionDir?: string;
|
|
17
17
|
/** The host's own extensions, built into every chat after turri's. */
|
|
18
18
|
extensions?: ExtensionFactory[];
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The dialogs a chat's questions are put in - ask_user's, a skill's interview, a review gate's - by the chat's id. Each
|
|
21
|
+
* is given a `signal`, aborted when its question is over without its answer - declined, or its turn stopped - for the
|
|
22
|
+
* host to close it.
|
|
23
|
+
*/
|
|
20
24
|
dialogs?: (chat: string) => AskUserIO | undefined;
|
|
21
25
|
/** What a chat's other UI calls - its notices, its working line - come to, by the chat's id. */
|
|
22
26
|
uiContext?: (chat: string) => ExtensionUIContext | undefined;
|
|
@@ -49,6 +53,13 @@ export interface Turri {
|
|
|
49
53
|
* A second interrupt straight after one that stopped something does nothing (`ignored`); `missing` for a chat not open.
|
|
50
54
|
*/
|
|
51
55
|
interrupt(id: string): Promise<"stopped" | "ignored" | "ended" | "missing">;
|
|
56
|
+
/**
|
|
57
|
+
* ctrl+c at a question, as the terminal has it: the question the chat asks now - its ask_user, a skill's interview, a
|
|
58
|
+
* review gate's escalation - declined. The asker is told the person declined to answer and the turn goes on; the
|
|
59
|
+
* host's dialog for it is told through its `signal`. Each question it is called at, however many in a row - it never
|
|
60
|
+
* stops the turn. `none` when the chat asks nothing now, `missing` for a chat not open.
|
|
61
|
+
*/
|
|
62
|
+
decline(id: string): Promise<"declined" | "none" | "missing">;
|
|
52
63
|
/** Told when a chat is opened, closed, renamed, or starts or stops working. */
|
|
53
64
|
onChats(listener: (chats: ChatInfo[]) => void): () => void;
|
|
54
65
|
/** Every chat let go, and what activate started stopped. */
|
package/dist/host-session.js
CHANGED
package/docs/chats.md
CHANGED
|
@@ -42,7 +42,7 @@ On macOS, `alt` is Option (⌥), and reaches turri only when the terminal sends
|
|
|
42
42
|
- **A hidden chat's questions** wait until you show it, then come up one after another. What it says in passing is said as you show it; what it keeps on the screen - a skill's progress, the job panel - is put back each time; what you had typed in its editor and not sent is still there.
|
|
43
43
|
- **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.
|
|
44
44
|
- **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.
|
|
45
|
-
- **`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)` ([hosting](hosting.md)).
|
|
45
|
+
- **`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)).
|
|
46
46
|
- **The last chat** is not closed: `/quit` leaves turri.
|
|
47
47
|
- **`/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.
|
|
48
48
|
- **A chat shown mid-turn** draws its reply as far as it is written, and follows it from there.
|
package/docs/hosting.md
CHANGED
|
@@ -77,7 +77,7 @@ process.chdir(project); // the session is the project's in the pr
|
|
|
77
77
|
const turri = await createTurri({
|
|
78
78
|
onJobChange: (job) => {}, // a job started, moved on, did something, or ended - any chat's
|
|
79
79
|
onPlanChange: () => {}, // what is planned changed with no job
|
|
80
|
-
dialogs: (chat) => ({ select, input }), // a chat's questions, put in the host's own dialogs
|
|
80
|
+
dialogs: (chat) => ({ select, input }), // a chat's questions, put in the host's own dialogs - each with a signal to close it
|
|
81
81
|
uiContext: (chat) => uiContext, // a chat's other UI calls - notices, the working line
|
|
82
82
|
surface: SURFACE, // the host's UI, in its own words, told to the model in place of the terminal's
|
|
83
83
|
systemPrompt: (prompt) => prompt, // the last word on the whole system prompt, each turn
|
|
@@ -90,6 +90,7 @@ chat.session.subscribe((event) => {}); // pi's session: its events, prompt(), ab
|
|
|
90
90
|
await chat.session.prompt("add a cart to the shop");
|
|
91
91
|
turri.focus(other.id); // the chat the person looks at now
|
|
92
92
|
await turri.interrupt(chat.id); // ctrl+c: "stopped" what it was doing, else "ended" the session - "ignored", "missing"
|
|
93
|
+
await turri.decline(chat.id); // ctrl+c at a question: "declined", the turn going on - "none", "missing"
|
|
93
94
|
turri.chats(); // [{ id, title, status: "idle" | "working" | "question" | "unread", shown }]
|
|
94
95
|
turri.onChats((chats) => {}); // a chat opened, closed, renamed, working or done
|
|
95
96
|
await turri.closeChat(chat.id); // its turn stopped, its conversation deleted - its pulses run on
|
|
@@ -99,10 +100,11 @@ await turri.dispose(); // every chat let go, what activate start
|
|
|
99
100
|
```
|
|
100
101
|
|
|
101
102
|
- **The chats.** Up to nine, each its own history, model, thinking level and stack, all on the session's one job registry and work loop - see [chats](chats.md). `openChat({ sessionFile })` reopens a saved one. A pulse's end is told to the chat that asked for it, its agent reacting there; to the chat `focus` last named when that one is closed. A chat is never hidden from turri's side: what the host does not show, it still runs, and its questions go to the host's dialogs at once - the host decides where they wait. `status` says a turn is running (`working`) or that it said something since it was last focused (`unread`).
|
|
102
|
-
- **Interrupting.** `interrupt(id)` is the terminal's ctrl+c for a host's key or button: it stops what that chat is doing - its turn, a skill or a tool in it, a `!` command, a compaction - and nothing in the other chats (`stopped`). A skill stopped keeps its run, to be continued. A question of the chat's open in the host's dialogs when its turn is stopped is over - declined -
|
|
103
|
+
- **Interrupting.** `interrupt(id)` is the terminal's ctrl+c for a host's key or button: it stops what that chat is doing - its turn, a skill or a tool in it, a `!` command, a compaction - and nothing in the other chats (`stopped`). A skill stopped keeps its run, to be continued. A question of the chat's open in the host's dialogs when its turn is stopped is over - declined - and its dialog's `signal` aborted, for the host to close it. A chat doing nothing ends the session, every chat with it, as `dispose` does (`ended`): turri never exits the host's process, so the host closes what showed it. An interrupt straight after one that stopped something is `ignored`, so pressing twice to be sure ends nothing. `session.abort()` is the chat's turn alone, and never ends the session.
|
|
104
|
+
- **Declining a question.** `decline(id)` is ctrl+c at a question in turri's terminal: the question the chat asks now - its ask_user, a skill's interview, a review gate's escalation, whatever goes through the host's dialogs - declined. The asker is told "The user declined to answer the question." and the turn goes on; a review gate declined stands. It declines each question it is called at, however many in a row, and never stops the turn - that is `interrupt`. `none` when the chat asks nothing now, `missing` for a chat not open. A dialog that resolves with `undefined` is something else - dismissed, and in a question of several answers "done" with the boxes ticked so far.
|
|
103
105
|
- **The record.** The chats are recorded in `.turri/chats.json` as the terminal records them, and `restore: true` reopens the last ended session's - without it, a host starts with none and deletes nothing. A chat closed is deleted; a session's chats are kept until five newer sessions have recorded theirs.
|
|
104
106
|
- **The jobs.** The host is told of each job as it changes, and has the registry and the loop themselves, so it shows the very jobs the chats run. Its buttons are best a chat's commands - `session.prompt("/regenerate app.plain")` - so the plan is the chats'.
|
|
105
|
-
- **The questions.** `dialogs(chat)` gives a chat's two dialogs: `select(title, options, context, at)` and `input(title, context)`, each resolving with the answer, or undefined when the person dismissed it. The chat's questions, a skill's interview and a review gate's escalation are put in them; without them, in the `uiContext`'s own dialogs.
|
|
107
|
+
- **The questions.** `dialogs(chat)` gives a chat's two dialogs: `select(title, options, context, at, { signal })` and `input(title, context, { signal })`, each resolving with the answer, or undefined when the person dismissed it. `signal` is aborted when the question is over without the dialog's answer - declined by `decline`, or its turn stopped (`interrupt`, `session.abort()`): the host closes the dialog it opened for it, whatever it resolves with then being ignored. A host may leave it unread. The chat's questions, a skill's interview and a review gate's escalation are put in them; without them, in the `uiContext`'s own dialogs.
|
|
106
108
|
- **The surface.** What turri tells the model of where the person watches the work is the terminal's by default: regen, the side panel on `ctrl+h`, `/pulses`. In a host none of that is there, and the model would send the person to keys and panels that do not exist. `surface` is the host's own description of its UI - the views it has in place of the terminal's, what its marks and buttons mean, where questions appear - in its own words. Given, turri leaves every terminal instruction out of what the model is told and puts the surface in turri's identity, as a `<surface>` block, where the identity speaks of regen. It reaches the side questions and the tools' own words too: a pulse not found says the `pulses` tool lists them, not `/pulses`. Text only the person sees in the terminal - the job panel, the side panel - is the terminal's and stays as it is. Without `surface`, the prompt is the terminal's, byte for byte. What the conversation is to do once something is released belongs there too - open it in the host's preview, say what to test: after a pulse released under `release-after-pulse`, the conversation does what it was told to do once something is released.
|
|
107
109
|
- **The system prompt.** `systemPrompt(prompt)` gets the whole system prompt for the turn - [pi](https://pi.dev)'s, turri's identity and blocks, the rules - after turri's own `before_agent_start` work, and returns what is sent. It is for what `surface` does not cover; to describe the UI, use `surface`, rather than words appended after turri's that contradict them.
|
|
108
110
|
- **The mode.** Each chat is bound in [pi](https://pi.dev)'s `tui` mode: the work runs in the background and keeps the chat free, as the terminal does. Its other UI calls - its notices, its working line, a confirm - come to `uiContext(chat)`, as [pi](https://pi.dev)'s [SDK](https://github.com/earendil-works/pi) describes it.
|