@agentshouse/kit 0.2.1-c7f53ff9eaf98 → 0.2.1-c83d468784cc9
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/README.md +28 -28
- package/dist/acp.js +90 -19
- package/dist/afk.js +17 -0
- package/dist/agents.js +87 -44
- package/dist/api.js +5 -5
- package/dist/authority-main.js +3 -3
- package/dist/bridge.js +5 -5
- package/dist/building.js +50 -0
- package/dist/calls.js +71 -0
- package/dist/chunks.js +49 -0
- package/dist/clis.js +45 -40
- package/dist/{stream.js → connection.js} +34 -52
- package/dist/conversations.js +498 -178
- package/dist/copies.js +315 -162
- package/dist/end.js +14 -4
- package/dist/events.js +66 -8
- package/dist/files.js +139 -37
- package/dist/git.js +47 -13
- package/dist/home.js +0 -7
- package/dist/house-cli.js +95 -87
- package/dist/house-main.js +3 -3
- package/dist/instructions.js +29 -9
- package/dist/kit-cli.js +4 -4
- package/dist/kit-main.js +3 -3
- package/dist/login.js +4 -4
- package/dist/logout.js +1 -1
- package/dist/mcp.js +6 -2
- package/dist/parts.js +10 -0
- package/dist/placement.js +21 -20
- package/dist/relay.js +21 -0
- package/dist/resident.js +51 -38
- package/dist/scope.js +15 -4
- package/dist/scripts.js +255 -0
- package/dist/secret-input.js +1 -1
- package/dist/service.js +23 -1
- package/dist/sign-in.js +22 -39
- package/dist/silence.js +28 -0
- package/dist/skills.js +42 -97
- package/dist/transcription.js +83 -43
- package/dist/translate.js +2 -8
- package/dist/update.js +113 -127
- package/dist/wire.js +37 -3
- package/package.json +20 -21
- package/dist/bootstrap.json +0 -24
- package/dist/clis-main.js +0 -23
- package/how-we-work.md +0 -33
- package/skills/ask-matt/LICENSE +0 -21
- package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
- package/skills/ask-matt/SKILL.md +0 -97
- package/skills/ask-matt/agents/openai.yaml +0 -5
- package/skills/code-review/LICENSE +0 -21
- package/skills/code-review/SKILL.md +0 -89
- package/skills/code-review/agents/openai.yaml +0 -3
- package/skills/codebase-design/DEEPENING.md +0 -37
- package/skills/codebase-design/DESIGN-IT-TWICE.md +0 -44
- package/skills/codebase-design/LICENSE +0 -21
- package/skills/codebase-design/SKILL.md +0 -114
- package/skills/codebase-design/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/LICENSE +0 -21
- package/skills/diagnosing-bugs/SKILL.md +0 -138
- package/skills/diagnosing-bugs/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +0 -44
- package/skills/domain-modeling/ADR-FORMAT.md +0 -47
- package/skills/domain-modeling/GLOSSARY-FORMAT.md +0 -60
- package/skills/domain-modeling/LICENSE +0 -21
- package/skills/domain-modeling/SKILL.md +0 -74
- package/skills/domain-modeling/agents/openai.yaml +0 -3
- package/skills/grill-me/LICENSE +0 -21
- package/skills/grill-me/SKILL.md +0 -7
- package/skills/grill-me/agents/openai.yaml +0 -5
- package/skills/grill-with-docs/LICENSE +0 -21
- package/skills/grill-with-docs/SKILL.md +0 -7
- package/skills/grill-with-docs/agents/openai.yaml +0 -5
- package/skills/grilling/LICENSE +0 -21
- package/skills/grilling/SKILL.md +0 -30
- package/skills/grilling/agents/openai.yaml +0 -3
- package/skills/handoff/LICENSE +0 -21
- package/skills/handoff/SKILL.md +0 -16
- package/skills/handoff/agents/openai.yaml +0 -5
- package/skills/implement/LICENSE +0 -21
- package/skills/implement/SKILL.md +0 -17
- package/skills/implement/agents/openai.yaml +0 -5
- package/skills/implement-spec/LICENSE +0 -21
- package/skills/implement-spec/SKILL.md +0 -40
- package/skills/implement-spec/agents/openai.yaml +0 -5
- package/skills/improve-codebase-architecture/HTML-REPORT.md +0 -123
- package/skills/improve-codebase-architecture/LICENSE +0 -21
- package/skills/improve-codebase-architecture/SKILL.md +0 -71
- package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
- package/skills/pr/CREDITS.md +0 -3
- package/skills/pr/LICENSE +0 -21
- package/skills/pr/SKILL.md +0 -170
- package/skills/pr/agents/openai.yaml +0 -3
- package/skills/prototype/LICENSE +0 -21
- package/skills/prototype/LOGIC.md +0 -67
- package/skills/prototype/SKILL.md +0 -26
- package/skills/prototype/UI.md +0 -112
- package/skills/prototype/agents/openai.yaml +0 -3
- package/skills/research/LICENSE +0 -21
- package/skills/research/SKILL.md +0 -12
- package/skills/research/agents/openai.yaml +0 -3
- package/skills/retro/LICENSE +0 -21
- package/skills/retro/SKILL.md +0 -44
- package/skills/retro/agents/openai.yaml +0 -5
- package/skills/tdd/LICENSE +0 -21
- package/skills/tdd/SKILL.md +0 -38
- package/skills/tdd/agents/openai.yaml +0 -3
- package/skills/tdd/mocking.md +0 -59
- package/skills/tdd/tests.md +0 -77
- package/skills/teach/GLOSSARY-FORMAT.md +0 -35
- package/skills/teach/LEARNING-RECORD-FORMAT.md +0 -46
- package/skills/teach/LICENSE +0 -21
- package/skills/teach/MISSION-FORMAT.md +0 -31
- package/skills/teach/RESOURCES-FORMAT.md +0 -32
- package/skills/teach/SKILL.md +0 -140
- package/skills/teach/agents/openai.yaml +0 -5
- package/skills/to-questionnaire/LICENSE +0 -21
- package/skills/to-questionnaire/SKILL.md +0 -54
- package/skills/to-questionnaire/agents/openai.yaml +0 -5
- package/skills/to-spec/LICENSE +0 -21
- package/skills/to-spec/SKILL.md +0 -75
- package/skills/to-spec/agents/openai.yaml +0 -5
- package/skills/to-tickets/LICENSE +0 -21
- package/skills/to-tickets/SKILL.md +0 -105
- package/skills/to-tickets/agents/openai.yaml +0 -5
- package/skills/triage/AGENT-BRIEF.md +0 -207
- package/skills/triage/LICENSE +0 -21
- package/skills/triage/OUT-OF-SCOPE.md +0 -105
- package/skills/triage/SKILL.md +0 -112
- package/skills/triage/agents/openai.yaml +0 -5
- package/skills/wait-what/LICENSE +0 -21
- package/skills/wait-what/SKILL.md +0 -7
- package/skills/wait-what/agents/openai.yaml +0 -5
- package/skills/wayfinder/LICENSE +0 -21
- package/skills/wayfinder/SKILL.md +0 -128
- package/skills/wayfinder/agents/openai.yaml +0 -5
- package/skills/wizard/LICENSE +0 -21
- package/skills/wizard/SKILL.md +0 -44
- package/skills/wizard/agents/openai.yaml +0 -3
- package/skills/wizard/template.sh +0 -223
- package/skills/writing-for-agents/LICENSE +0 -21
- package/skills/writing-for-agents/SKILL-MECHANICS.md +0 -22
- package/skills/writing-for-agents/SKILL.md +0 -81
- package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/conversations.js
CHANGED
|
@@ -3,37 +3,60 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { unlessAborted } from './abort.js';
|
|
6
|
-
import { startAdapter } from './acp.js';
|
|
6
|
+
import { signedOut, startAdapter } from './acp.js';
|
|
7
|
+
import { formAnswered, grokQuestionAnswered, permissionAllowed, PLAN_APPROVED } from './afk.js';
|
|
8
|
+
import { finalRefusal, HouseRefusal } from './api.js';
|
|
7
9
|
import { openBridge } from './bridge.js';
|
|
8
10
|
import { CLIS } from './clis.js';
|
|
9
11
|
import { answeredFailure, readerFor } from './events.js';
|
|
10
|
-
import {
|
|
12
|
+
import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
|
|
11
13
|
import { agentBase } from './home.js';
|
|
12
14
|
import { instructions } from './instructions.js';
|
|
13
|
-
import { Parts } from './parts.js';
|
|
15
|
+
import { addedTokens, Parts } from './parts.js';
|
|
16
|
+
import { refusalOf } from './refusals.js';
|
|
17
|
+
import { relayed } from './relay.js';
|
|
14
18
|
import { killMarked, Scope } from './scope.js';
|
|
15
19
|
import { holdSecretInput } from './secret-input.js';
|
|
16
|
-
import { createHowWeWork } from './skills.js';
|
|
17
20
|
import { transcribe } from './transcription.js';
|
|
18
21
|
import { workFor } from './work.js';
|
|
19
|
-
const SIGNED_OUT = RequestError.authRequired().code;
|
|
20
22
|
const KILLED = 'the conversation was killed';
|
|
23
|
+
const INTERRUPTED = 'the conversation was interrupted';
|
|
21
24
|
const REFUSED = 'the CLI refused the prompt';
|
|
22
|
-
const NO_CONNECTION = "the
|
|
25
|
+
const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
|
|
26
|
+
const GONE = 'House no longer holds this conversation; start a new one';
|
|
27
|
+
const RETIRED = "this conversation's Agent is retired, so it cannot start";
|
|
28
|
+
const ENDED = {
|
|
29
|
+
conversation_not_found: GONE,
|
|
30
|
+
conversation_historical: RETIRED,
|
|
31
|
+
};
|
|
23
32
|
const NO_START = "the Agent's CLI did not start";
|
|
24
33
|
const NO_SESSION = "the Agent's CLI did not open a session";
|
|
25
34
|
const NO_RESUME = "the Agent's CLI could not resume this conversation";
|
|
35
|
+
const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
|
|
26
36
|
const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
|
|
37
|
+
const UNSTEERED = "the Agent's CLI did not take the message into its running turn";
|
|
38
|
+
// A reader tells a working run from a stopped one by the minute, and House keeps no finer figure, so a run reports no oftener.
|
|
39
|
+
const ACTIVITY_REPORT_MS = 60_000;
|
|
27
40
|
class NotStarted extends Error {
|
|
28
41
|
}
|
|
42
|
+
class ResumeRefused extends NotStarted {
|
|
43
|
+
}
|
|
29
44
|
async function step(reason, work) {
|
|
30
45
|
try {
|
|
31
46
|
return await work;
|
|
32
47
|
}
|
|
33
48
|
catch (error) {
|
|
34
|
-
throw new NotStarted(reason, { cause: error });
|
|
49
|
+
throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
|
|
35
50
|
}
|
|
36
51
|
}
|
|
52
|
+
function unopened(reason) {
|
|
53
|
+
return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
|
|
54
|
+
}
|
|
55
|
+
// A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
|
|
56
|
+
const STARTING_SHOWN_AFTER_MS = 1_000;
|
|
57
|
+
function unbridged(error) {
|
|
58
|
+
return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
|
|
59
|
+
}
|
|
37
60
|
const EXIT_PLAN_MODE = {
|
|
38
61
|
parse(value) {
|
|
39
62
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
@@ -42,41 +65,64 @@ const EXIT_PLAN_MODE = {
|
|
|
42
65
|
return value;
|
|
43
66
|
},
|
|
44
67
|
};
|
|
45
|
-
|
|
68
|
+
const GROK_QUESTION = {
|
|
69
|
+
parse(value) {
|
|
70
|
+
return value;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
|
|
46
74
|
function causeOf(error) {
|
|
47
75
|
return error instanceof Error ? error.message : String(error);
|
|
48
76
|
}
|
|
49
77
|
function answeredWith(error) {
|
|
50
|
-
|
|
51
|
-
return { failure: signedOut ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
78
|
+
return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
52
79
|
}
|
|
53
80
|
function exitedWith(running, cause) {
|
|
54
|
-
|
|
81
|
+
if (running.killed)
|
|
82
|
+
return { failure: 'killed', cause: KILLED };
|
|
83
|
+
return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
|
|
55
84
|
}
|
|
56
85
|
function answerMessage(request, response) {
|
|
57
86
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
58
87
|
}
|
|
88
|
+
async function sentPart(url, bytes, headers) {
|
|
89
|
+
const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
|
|
90
|
+
await answer.arrayBuffer();
|
|
91
|
+
if (!answer.ok)
|
|
92
|
+
throw storeRefusal('upload', answer.status);
|
|
93
|
+
}
|
|
59
94
|
function logged(error) {
|
|
60
95
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
61
96
|
}
|
|
62
97
|
function refusal(conversation, error) {
|
|
63
98
|
if (conversation.kills > 0)
|
|
64
99
|
return KILLED;
|
|
65
|
-
const cause = error instanceof
|
|
100
|
+
const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
|
|
66
101
|
logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
|
|
67
102
|
return causeOf(error);
|
|
68
103
|
}
|
|
104
|
+
function audioOf(result) {
|
|
105
|
+
if (result.isError === true)
|
|
106
|
+
return null;
|
|
107
|
+
const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
|
|
108
|
+
return file?.media_type?.startsWith('audio/') === true ? file : null;
|
|
109
|
+
}
|
|
69
110
|
function marker(bridge) {
|
|
70
111
|
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
71
112
|
}
|
|
113
|
+
function steers(adapter) {
|
|
114
|
+
return adapter.initialized._meta?.steering?.supported === true;
|
|
115
|
+
}
|
|
72
116
|
class Turn {
|
|
73
117
|
id = randomUUID();
|
|
74
118
|
parts;
|
|
75
|
-
admitted = false;
|
|
76
119
|
questions = 0;
|
|
77
120
|
secrets = 0;
|
|
78
121
|
prompted = false;
|
|
122
|
+
steered = false;
|
|
123
|
+
taken = null;
|
|
79
124
|
ended = false;
|
|
125
|
+
quietUntil = Infinity;
|
|
80
126
|
images = 0;
|
|
81
127
|
uploads = [];
|
|
82
128
|
constructor(emit) {
|
|
@@ -89,21 +135,33 @@ class Conversation {
|
|
|
89
135
|
session = null;
|
|
90
136
|
turn = null;
|
|
91
137
|
commands = null;
|
|
138
|
+
options = null;
|
|
139
|
+
exitUntold = false;
|
|
140
|
+
afk = false;
|
|
92
141
|
context = null;
|
|
142
|
+
tokens = null;
|
|
93
143
|
leftovers = new Set();
|
|
94
144
|
waiting = [];
|
|
145
|
+
done = false;
|
|
95
146
|
sending = null;
|
|
96
147
|
subagents = new Map();
|
|
97
148
|
held = [];
|
|
98
149
|
opening = null;
|
|
150
|
+
bridge = null;
|
|
151
|
+
launch = null;
|
|
99
152
|
prompted = null;
|
|
100
153
|
kills = 0;
|
|
101
154
|
killed = new AbortController();
|
|
102
155
|
interrupts = 0;
|
|
103
156
|
interrupted = new AbortController();
|
|
104
|
-
|
|
157
|
+
turns = new Set();
|
|
105
158
|
queue = Promise.resolve();
|
|
106
159
|
reports = Promise.resolve();
|
|
160
|
+
starting = null;
|
|
161
|
+
preparing = null;
|
|
162
|
+
told = null;
|
|
163
|
+
untold = false;
|
|
164
|
+
shown = null;
|
|
107
165
|
constructor(id) {
|
|
108
166
|
this.id = id;
|
|
109
167
|
}
|
|
@@ -119,9 +177,7 @@ function asksSecret(request) {
|
|
|
119
177
|
export class Conversations {
|
|
120
178
|
kit;
|
|
121
179
|
conversations = new Map();
|
|
122
|
-
acks = new Map();
|
|
123
180
|
questions = new Map();
|
|
124
|
-
carrying = 0;
|
|
125
181
|
reporting = 0;
|
|
126
182
|
watched = false;
|
|
127
183
|
constructor(kit) {
|
|
@@ -129,43 +185,34 @@ export class Conversations {
|
|
|
129
185
|
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
130
186
|
setInterval(() => this.look(), 2_000);
|
|
131
187
|
}
|
|
132
|
-
|
|
133
|
-
this.
|
|
134
|
-
|
|
135
|
-
(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
this.acks.set(input.input_id, work);
|
|
152
|
-
return work;
|
|
153
|
-
})();
|
|
154
|
-
carried
|
|
155
|
-
.then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
|
|
156
|
-
.catch(logged)
|
|
157
|
-
.finally(() => {
|
|
158
|
-
this.carrying--;
|
|
159
|
-
this.kit.changed();
|
|
188
|
+
call(call) {
|
|
189
|
+
void this.kit.calls.carry(call.input_id, () => {
|
|
190
|
+
const conversation = this.conversation(call.conversation_id);
|
|
191
|
+
if (call.kind === 'open' || call.kind === 'message')
|
|
192
|
+
conversation.afk = call.afk === true;
|
|
193
|
+
if (call.kind === 'kill') {
|
|
194
|
+
conversation.kills++;
|
|
195
|
+
conversation.killed.abort(new Error(KILLED));
|
|
196
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
197
|
+
waiting.settle({ refused: KILLED });
|
|
198
|
+
this.halt(conversation);
|
|
199
|
+
}
|
|
200
|
+
if (call.kind === 'interrupt') {
|
|
201
|
+
conversation.interrupts++;
|
|
202
|
+
conversation.interrupted.abort();
|
|
203
|
+
}
|
|
204
|
+
const handled = conversation.queue.then(() => this.carry(conversation, call));
|
|
205
|
+
conversation.queue = handled.catch(() => undefined);
|
|
206
|
+
return handled.then(async ({ result }) => ({ result: await result }));
|
|
160
207
|
});
|
|
161
208
|
}
|
|
162
209
|
idle() {
|
|
163
|
-
if (this.
|
|
210
|
+
if (this.reporting > 0)
|
|
164
211
|
return false;
|
|
165
212
|
for (const conversation of this.conversations.values()) {
|
|
166
213
|
const turn = conversation.turn;
|
|
167
214
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
168
|
-
if (conversation.
|
|
215
|
+
if (conversation.turns.size > (asking ? 1 : 0))
|
|
169
216
|
return false;
|
|
170
217
|
}
|
|
171
218
|
const conversations = [...this.conversations.values()];
|
|
@@ -174,7 +221,7 @@ export class Conversations {
|
|
|
174
221
|
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
175
222
|
}
|
|
176
223
|
quiet() {
|
|
177
|
-
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.
|
|
224
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
|
|
178
225
|
}
|
|
179
226
|
look() {
|
|
180
227
|
for (const conversation of this.conversations.values()) {
|
|
@@ -189,6 +236,16 @@ export class Conversations {
|
|
|
189
236
|
}
|
|
190
237
|
opened() {
|
|
191
238
|
this.watched = false;
|
|
239
|
+
for (const conversation of this.conversations.values()) {
|
|
240
|
+
if (conversation.running !== null) {
|
|
241
|
+
this.facts(conversation);
|
|
242
|
+
this.subagentsChanged(conversation);
|
|
243
|
+
}
|
|
244
|
+
else if (conversation.exitUntold)
|
|
245
|
+
this.process(conversation, false);
|
|
246
|
+
if (conversation.untold || conversation.told !== null)
|
|
247
|
+
this.retell(conversation);
|
|
248
|
+
}
|
|
192
249
|
}
|
|
193
250
|
watching(watched) {
|
|
194
251
|
const resend = watched && !this.watched;
|
|
@@ -208,134 +265,319 @@ export class Conversations {
|
|
|
208
265
|
}
|
|
209
266
|
return conversation;
|
|
210
267
|
}
|
|
211
|
-
async carry(conversation,
|
|
212
|
-
if (
|
|
213
|
-
return
|
|
214
|
-
if (
|
|
215
|
-
return this.message(conversation,
|
|
216
|
-
if (
|
|
268
|
+
async carry(conversation, call) {
|
|
269
|
+
if (call.kind === 'open')
|
|
270
|
+
return this.open(conversation, call);
|
|
271
|
+
if (call.kind === 'message')
|
|
272
|
+
return this.message(conversation, call);
|
|
273
|
+
if (call.kind === 'done') {
|
|
274
|
+
conversation.done = true;
|
|
275
|
+
this.next(conversation);
|
|
276
|
+
return { result: Promise.resolve({}) };
|
|
277
|
+
}
|
|
278
|
+
await this.launched(conversation);
|
|
279
|
+
if (call.kind === 'interrupt') {
|
|
217
280
|
if (--conversation.interrupts === 0)
|
|
218
281
|
conversation.interrupted = new AbortController();
|
|
219
|
-
await this.interrupt(conversation, String(
|
|
282
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
220
283
|
}
|
|
221
|
-
if (
|
|
284
|
+
if (call.kind === 'kill') {
|
|
222
285
|
await this.kill(conversation);
|
|
223
286
|
await conversation.sending;
|
|
224
287
|
if (--conversation.kills === 0)
|
|
225
288
|
conversation.killed = new AbortController();
|
|
226
289
|
}
|
|
227
|
-
if (
|
|
228
|
-
await this.option(conversation, String(
|
|
229
|
-
if (
|
|
230
|
-
const question = this.questions.get(String(
|
|
290
|
+
if (call.kind === 'option')
|
|
291
|
+
await this.option(conversation, String(call.option), call.value);
|
|
292
|
+
if (call.kind === 'answer') {
|
|
293
|
+
const question = this.questions.get(String(call.interaction_id));
|
|
231
294
|
if (question === undefined) {
|
|
232
|
-
const text = answerMessage(
|
|
233
|
-
return this.message(conversation, { ...
|
|
295
|
+
const text = answerMessage(call.request, call.response);
|
|
296
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
234
297
|
}
|
|
235
|
-
question.answer(
|
|
298
|
+
question.answer(call.response);
|
|
236
299
|
}
|
|
237
|
-
return {
|
|
300
|
+
return { result: Promise.resolve({}) };
|
|
301
|
+
}
|
|
302
|
+
async open(conversation, call) {
|
|
303
|
+
await this.stop(conversation);
|
|
304
|
+
const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
|
|
305
|
+
return {
|
|
306
|
+
result: launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) })),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async stop(conversation) {
|
|
310
|
+
const running = conversation.running;
|
|
311
|
+
running?.scope.kill();
|
|
312
|
+
await running?.adapter.exited;
|
|
238
313
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
314
|
+
launch(conversation, route, sessionId, empty) {
|
|
315
|
+
let opened;
|
|
316
|
+
let failed;
|
|
317
|
+
const bridge = new Promise((resolve, reject) => {
|
|
318
|
+
opened = resolve;
|
|
319
|
+
failed = reject;
|
|
320
|
+
});
|
|
321
|
+
bridge.catch(() => undefined);
|
|
322
|
+
const session = this.start(conversation, route, sessionId, empty, opened);
|
|
323
|
+
session.catch(failed);
|
|
324
|
+
const launch = { bridge, session };
|
|
325
|
+
conversation.launch = launch;
|
|
326
|
+
void session
|
|
327
|
+
.catch(() => undefined)
|
|
328
|
+
.then(() => {
|
|
329
|
+
if (conversation.launch !== launch)
|
|
330
|
+
return;
|
|
331
|
+
conversation.launch = null;
|
|
332
|
+
this.closeIdleBridge(conversation);
|
|
333
|
+
this.next(conversation);
|
|
334
|
+
});
|
|
335
|
+
this.next(conversation);
|
|
336
|
+
return launch;
|
|
337
|
+
}
|
|
338
|
+
async launched(conversation) {
|
|
339
|
+
while (conversation.launch !== null)
|
|
340
|
+
await conversation.launch.session.catch(() => undefined);
|
|
341
|
+
}
|
|
342
|
+
instructed(conversation, route) {
|
|
343
|
+
const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
|
|
344
|
+
if (bridge === undefined)
|
|
345
|
+
return null;
|
|
346
|
+
const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
|
|
347
|
+
read.catch(() => undefined);
|
|
348
|
+
return read;
|
|
349
|
+
}
|
|
350
|
+
async message(conversation, call) {
|
|
351
|
+
const content = [
|
|
352
|
+
...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
|
|
353
|
+
{ type: 'text', text: String(call.text) },
|
|
354
|
+
];
|
|
355
|
+
let route;
|
|
356
|
+
let instructing = null;
|
|
242
357
|
try {
|
|
243
|
-
|
|
358
|
+
route = await this.route(conversation, call.agent_id);
|
|
359
|
+
if (call.first === true)
|
|
360
|
+
instructing = this.instructed(conversation, route);
|
|
361
|
+
if (call.message_id !== null)
|
|
362
|
+
content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
|
|
363
|
+
if (conversation.kills > 0)
|
|
364
|
+
throw new Error(KILLED);
|
|
244
365
|
}
|
|
245
366
|
catch (error) {
|
|
246
|
-
|
|
367
|
+
this.closeIdleBridge(conversation);
|
|
368
|
+
this.prepares(conversation, null);
|
|
369
|
+
return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
247
370
|
}
|
|
371
|
+
conversation.preparing = null;
|
|
372
|
+
const { promise, resolve: settle } = Promise.withResolvers();
|
|
373
|
+
const waiting = { call, route, content, instructing, settle };
|
|
374
|
+
if (call.at_once === true && conversation.turns.size > 0)
|
|
375
|
+
await this.atOnce(conversation, waiting);
|
|
376
|
+
else
|
|
377
|
+
conversation.waiting.push(waiting);
|
|
378
|
+
this.next(conversation);
|
|
379
|
+
return { result: promise };
|
|
248
380
|
}
|
|
249
|
-
async
|
|
250
|
-
|
|
251
|
-
|
|
381
|
+
async atOnce(conversation, waiting) {
|
|
382
|
+
while (conversation.sending !== null)
|
|
383
|
+
await conversation.sending;
|
|
384
|
+
const running = conversation.running;
|
|
385
|
+
if (running !== null && conversation.turns.size > 0 && steers(running.adapter)) {
|
|
386
|
+
conversation.sending = this.steer(conversation, running, waiting);
|
|
387
|
+
await conversation.sending;
|
|
388
|
+
conversation.sending = null;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
this.ahead(conversation, waiting);
|
|
392
|
+
if (conversation.turns.size > 0)
|
|
393
|
+
await this.cancel(conversation);
|
|
394
|
+
}
|
|
395
|
+
ahead(conversation, waiting) {
|
|
396
|
+
const behind = conversation.waiting.findIndex((held) => held.call.at_once !== true);
|
|
397
|
+
conversation.waiting.splice(behind === -1 ? conversation.waiting.length : behind, 0, waiting);
|
|
398
|
+
}
|
|
399
|
+
async steer(conversation, running, waiting) {
|
|
400
|
+
let outcome;
|
|
252
401
|
try {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
content.push({ type: 'text', text: lines.join('\n') });
|
|
259
|
-
if (conversation.kills > 0)
|
|
260
|
-
throw new Error(KILLED);
|
|
402
|
+
({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
|
|
403
|
+
sessionId: running.sessionId,
|
|
404
|
+
prompt: waiting.content,
|
|
405
|
+
_meta: { steering: { idleBehavior: 'promptRequired' } },
|
|
406
|
+
}));
|
|
261
407
|
}
|
|
262
408
|
catch (error) {
|
|
263
|
-
|
|
409
|
+
outcome = error;
|
|
410
|
+
}
|
|
411
|
+
if (outcome === 'promptRequired') {
|
|
412
|
+
this.ahead(conversation, waiting);
|
|
413
|
+
}
|
|
414
|
+
else if (outcome === 'injected' || outcome === 'startedNewTurn') {
|
|
415
|
+
if (outcome === 'injected') {
|
|
416
|
+
for (const turn of conversation.turns)
|
|
417
|
+
turn.steered ||= turn.prompted;
|
|
418
|
+
}
|
|
419
|
+
this.kit.calls.keepResult(String(waiting.call.input_id), {});
|
|
420
|
+
waiting.settle({});
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
const cause = outcome instanceof Error ? outcome : new Error(`the CLI answered ${JSON.stringify(outcome)}`);
|
|
424
|
+
waiting.settle({ refused: refusal(conversation, new NotStarted(UNSTEERED, { cause })) });
|
|
264
425
|
}
|
|
265
|
-
return {
|
|
266
|
-
ack: new Promise((settle) => {
|
|
267
|
-
conversation.waiting.push({ input, route, content, settle });
|
|
268
|
-
this.next(conversation);
|
|
269
|
-
}),
|
|
270
|
-
};
|
|
271
426
|
}
|
|
272
427
|
next(conversation) {
|
|
273
|
-
if (conversation.sending !== null || conversation.
|
|
428
|
+
if (conversation.sending !== null || conversation.turns.size > 0)
|
|
274
429
|
return;
|
|
275
430
|
const waiting = conversation.waiting.shift();
|
|
276
|
-
if (waiting === undefined)
|
|
431
|
+
if (waiting === undefined) {
|
|
432
|
+
const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
|
|
433
|
+
if (conversation.starting !== step)
|
|
434
|
+
this.starts(conversation, step);
|
|
435
|
+
if (conversation.done && conversation.launch === null)
|
|
436
|
+
void this.stop(conversation).catch(logged);
|
|
277
437
|
return;
|
|
438
|
+
}
|
|
278
439
|
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
279
440
|
conversation.sending = null;
|
|
280
441
|
this.next(conversation);
|
|
281
442
|
});
|
|
282
443
|
}
|
|
283
444
|
async send(conversation, waiting) {
|
|
284
|
-
const {
|
|
285
|
-
let
|
|
445
|
+
const { call } = waiting;
|
|
446
|
+
let result = {};
|
|
286
447
|
let started = false;
|
|
448
|
+
let instructing = waiting.instructing;
|
|
287
449
|
const prompt = [];
|
|
288
450
|
try {
|
|
289
451
|
if (conversation.running === null) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
452
|
+
this.starts(conversation, 'session');
|
|
453
|
+
await this.launched(conversation);
|
|
454
|
+
}
|
|
455
|
+
if (conversation.running === null) {
|
|
456
|
+
const stored = conversation.session ?? call.provider_session_id;
|
|
457
|
+
const empty = call.first === true && conversation.prompted !== stored;
|
|
458
|
+
const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
|
|
459
|
+
if (call.first === true)
|
|
460
|
+
instructing = this.instructed(conversation, waiting.route);
|
|
461
|
+
started = (await launch.session) !== stored;
|
|
293
462
|
}
|
|
294
463
|
const session = conversation.running.sessionId;
|
|
295
|
-
if (started || (
|
|
296
|
-
|
|
464
|
+
if (started || (call.first === true && session !== call.provider_session_id)) {
|
|
465
|
+
result = { provider_session_id: session };
|
|
297
466
|
}
|
|
298
|
-
if (
|
|
299
|
-
prompt.push({
|
|
300
|
-
type: 'text',
|
|
301
|
-
text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
|
|
302
|
-
});
|
|
467
|
+
if (call.first === true) {
|
|
468
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
303
469
|
}
|
|
304
470
|
if (conversation.kills > 0)
|
|
305
471
|
throw new Error(KILLED);
|
|
306
472
|
}
|
|
307
473
|
catch (error) {
|
|
474
|
+
this.starts(conversation, null);
|
|
308
475
|
if (started) {
|
|
309
476
|
conversation.session = null;
|
|
310
477
|
await this.kill(conversation);
|
|
311
478
|
}
|
|
312
|
-
|
|
479
|
+
const refused = refusal(conversation, error);
|
|
480
|
+
const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
|
|
481
|
+
waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
|
|
313
482
|
return;
|
|
314
483
|
}
|
|
315
|
-
this.
|
|
316
|
-
waiting.
|
|
484
|
+
this.starts(conversation, null);
|
|
485
|
+
this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
|
|
486
|
+
waiting.settle(result);
|
|
317
487
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
488
|
+
starts(conversation, step) {
|
|
489
|
+
conversation.starting = step;
|
|
490
|
+
if (conversation.shown !== null)
|
|
491
|
+
clearTimeout(conversation.shown);
|
|
492
|
+
conversation.shown = null;
|
|
493
|
+
if (step === null || conversation.told !== null) {
|
|
494
|
+
this.tell(conversation, step);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
conversation.shown = setTimeout(() => {
|
|
498
|
+
conversation.shown = null;
|
|
499
|
+
if (conversation.starting === step)
|
|
500
|
+
this.tell(conversation, step);
|
|
501
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
502
|
+
}
|
|
503
|
+
prepares(conversation, step) {
|
|
504
|
+
conversation.preparing = step;
|
|
505
|
+
this.next(conversation);
|
|
506
|
+
}
|
|
507
|
+
tell(conversation, step) {
|
|
508
|
+
if (conversation.told === step && !conversation.untold)
|
|
509
|
+
return;
|
|
510
|
+
conversation.told = step;
|
|
511
|
+
this.retell(conversation);
|
|
512
|
+
}
|
|
513
|
+
retell(conversation) {
|
|
514
|
+
conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
|
|
515
|
+
}
|
|
516
|
+
async transcribed(conversation, messageId, text) {
|
|
517
|
+
const paths = linkedPaths(text);
|
|
518
|
+
if (paths.length === 0)
|
|
519
|
+
return [];
|
|
520
|
+
const killed = conversation.killed.signal;
|
|
521
|
+
const interrupted = conversation.interrupted.signal;
|
|
522
|
+
const transcribing = AbortSignal.any([killed, interrupted]);
|
|
523
|
+
let signal = killed;
|
|
524
|
+
const blocks = [];
|
|
525
|
+
try {
|
|
526
|
+
const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
|
|
527
|
+
const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
|
|
528
|
+
for (const path of paths) {
|
|
529
|
+
let answered = await read(path);
|
|
530
|
+
if (answered === null)
|
|
531
|
+
continue;
|
|
532
|
+
signal = transcribing;
|
|
533
|
+
this.prepares(conversation, 'transcription');
|
|
534
|
+
const said = await transcribe(async () => {
|
|
535
|
+
const address = answered ?? (await read(path));
|
|
536
|
+
answered = null;
|
|
537
|
+
if (address === null)
|
|
538
|
+
throw new Error(`${path} is no longer an audio file`);
|
|
539
|
+
return address;
|
|
540
|
+
}, signal);
|
|
541
|
+
await step(unbridged, unlessAborted(this.kit.house.deliver('/kit/transcripts/keep', { conversation: conversation.id, message: messageId, path, text: said }, () => !signal.aborted), signal));
|
|
542
|
+
blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
|
|
333
543
|
}
|
|
334
544
|
}
|
|
335
|
-
|
|
545
|
+
catch (error) {
|
|
546
|
+
if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
|
|
547
|
+
throw new NotStarted(INTERRUPTED, { cause: error });
|
|
548
|
+
throw error;
|
|
549
|
+
}
|
|
550
|
+
return blocks;
|
|
551
|
+
}
|
|
552
|
+
bridged(conversation) {
|
|
553
|
+
if (conversation.bridge === null) {
|
|
554
|
+
const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
555
|
+
conversation.bridge = bridging;
|
|
556
|
+
bridging.catch(() => {
|
|
557
|
+
if (conversation.bridge === bridging)
|
|
558
|
+
conversation.bridge = null;
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
return conversation.bridge;
|
|
562
|
+
}
|
|
563
|
+
closeBridge(conversation, bridging, bridge) {
|
|
564
|
+
bridge.close();
|
|
565
|
+
if (conversation.bridge === bridging)
|
|
566
|
+
conversation.bridge = null;
|
|
567
|
+
}
|
|
568
|
+
closeIdleBridge(conversation) {
|
|
569
|
+
const bridging = conversation.bridge;
|
|
570
|
+
if (bridging === null || conversation.running !== null || conversation.launch !== null)
|
|
571
|
+
return;
|
|
572
|
+
conversation.bridge = null;
|
|
573
|
+
void bridging.then((bridge) => bridge.close(), () => undefined);
|
|
336
574
|
}
|
|
337
575
|
async interrupt(conversation, turnId) {
|
|
338
|
-
if (conversation.
|
|
576
|
+
if ([...conversation.turns].some((turn) => turn.id === turnId))
|
|
577
|
+
await this.cancel(conversation);
|
|
578
|
+
}
|
|
579
|
+
async cancel(conversation) {
|
|
580
|
+
if (conversation.running === null)
|
|
339
581
|
return;
|
|
340
582
|
await conversation.running.adapter.connection.agent.notify('session/cancel', {
|
|
341
583
|
sessionId: conversation.running.sessionId,
|
|
@@ -387,58 +629,65 @@ export class Conversations {
|
|
|
387
629
|
}
|
|
388
630
|
return route;
|
|
389
631
|
}
|
|
390
|
-
async start(conversation,
|
|
632
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
633
|
+
const route = await routed;
|
|
391
634
|
const reader = readerFor(route.kind);
|
|
392
635
|
reader.session = sessionId;
|
|
393
636
|
const app = client({ name: '@agentshouse/kit' })
|
|
394
|
-
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
395
|
-
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
396
|
-
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal))
|
|
637
|
+
.onRequest('session/request_permission', ({ params, signal }) => conversation.afk ? permissionAllowed(params) : this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
638
|
+
.onRequest('elicitation/create', ({ params, signal }) => conversation.afk ? formAnswered(route.kind, params) : this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
639
|
+
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => conversation.afk ? PLAN_APPROVED : this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal))
|
|
640
|
+
.onRequest('_x.ai/ask_user_question', GROK_QUESTION, ({ params }) => {
|
|
641
|
+
if (!conversation.afk)
|
|
642
|
+
throw RequestError.methodNotFound('_x.ai/ask_user_question');
|
|
643
|
+
return grokQuestionAnswered(params);
|
|
644
|
+
});
|
|
397
645
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
398
|
-
const
|
|
646
|
+
const bridging = this.bridged(conversation);
|
|
647
|
+
const bridge = await step(unbridged, bridging);
|
|
399
648
|
if (conversation.kills > 0) {
|
|
400
|
-
|
|
649
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
401
650
|
throw new Error(KILLED);
|
|
402
651
|
}
|
|
403
652
|
conversation.opening = bridge;
|
|
404
653
|
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
405
654
|
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
655
|
+
let adapter = null;
|
|
406
656
|
try {
|
|
407
|
-
|
|
657
|
+
bridged(bridge);
|
|
408
658
|
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
409
659
|
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
410
|
-
|
|
660
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
411
661
|
work.read(message);
|
|
412
662
|
this.read(conversation, reader, message);
|
|
413
663
|
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
414
|
-
void adapter.exited.then(() =>
|
|
664
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
415
665
|
const agent = adapter.connection.agent;
|
|
416
666
|
const cwd = route.working_directory;
|
|
417
667
|
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
418
668
|
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
419
|
-
const opened = resumed ?? (await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
669
|
+
const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
420
670
|
reader.session = opened.sessionId;
|
|
421
671
|
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
422
672
|
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
423
|
-
await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
673
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
424
674
|
}
|
|
425
|
-
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
675
|
+
const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
426
676
|
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
427
677
|
void work.settled().then(() => scope.record());
|
|
428
678
|
conversation.running = running;
|
|
679
|
+
adapter.turn(conversation.turns.size > 0);
|
|
429
680
|
conversation.session = running.sessionId;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
this.options(conversation, options);
|
|
434
|
-
void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
|
|
681
|
+
conversation.options = options;
|
|
682
|
+
this.facts(conversation);
|
|
683
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
435
684
|
return running.sessionId;
|
|
436
685
|
}
|
|
437
686
|
catch (error) {
|
|
438
|
-
|
|
687
|
+
adapter?.retire();
|
|
439
688
|
work.close();
|
|
440
689
|
conversation.leftovers.add(scope);
|
|
441
|
-
|
|
690
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
442
691
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
443
692
|
}
|
|
444
693
|
finally {
|
|
@@ -451,7 +700,7 @@ export class Conversations {
|
|
|
451
700
|
}
|
|
452
701
|
catch (error) {
|
|
453
702
|
if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
|
|
454
|
-
throw new NotStarted(NO_RESUME, { cause: error });
|
|
703
|
+
throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
|
|
455
704
|
}
|
|
456
705
|
logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
|
|
457
706
|
return null;
|
|
@@ -478,10 +727,7 @@ export class Conversations {
|
|
|
478
727
|
}
|
|
479
728
|
return options;
|
|
480
729
|
}
|
|
481
|
-
exited(conversation,
|
|
482
|
-
const running = conversation.running;
|
|
483
|
-
if (running?.adapter !== adapter)
|
|
484
|
-
return;
|
|
730
|
+
exited(conversation, running, cause) {
|
|
485
731
|
this.detach(conversation, running);
|
|
486
732
|
if (conversation.turn !== null) {
|
|
487
733
|
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
@@ -490,15 +736,36 @@ export class Conversations {
|
|
|
490
736
|
this.kit.changed();
|
|
491
737
|
}
|
|
492
738
|
detach(conversation, running) {
|
|
739
|
+
if (conversation.bridge === running.bridging)
|
|
740
|
+
conversation.bridge = null;
|
|
493
741
|
running.work.close();
|
|
494
742
|
conversation.leftovers.add(running.scope);
|
|
495
743
|
conversation.running = null;
|
|
496
744
|
conversation.commands = null;
|
|
745
|
+
conversation.options = null;
|
|
746
|
+
if (conversation.subagents.size > 0) {
|
|
747
|
+
conversation.subagents.clear();
|
|
748
|
+
this.subagentsChanged(conversation);
|
|
749
|
+
}
|
|
497
750
|
for (const [id, question] of this.questions) {
|
|
498
751
|
if (question.conversation === conversation)
|
|
499
752
|
this.questions.delete(id);
|
|
500
753
|
}
|
|
501
|
-
this.
|
|
754
|
+
this.process(conversation, false);
|
|
755
|
+
}
|
|
756
|
+
process(conversation, running) {
|
|
757
|
+
conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
|
|
758
|
+
}
|
|
759
|
+
facts(conversation) {
|
|
760
|
+
this.process(conversation, true);
|
|
761
|
+
if (conversation.commands !== null)
|
|
762
|
+
this.commands(conversation, conversation.commands);
|
|
763
|
+
if (conversation.options !== null)
|
|
764
|
+
this.options(conversation, conversation.options);
|
|
765
|
+
}
|
|
766
|
+
subagentsChanged(conversation) {
|
|
767
|
+
const subagents = [...conversation.subagents.values()].map(({ description, activity }) => ({ description, activity }));
|
|
768
|
+
this.kit.send({ type: 'subagents', conversation_id: conversation.id, subagents });
|
|
502
769
|
}
|
|
503
770
|
commands(conversation, commands) {
|
|
504
771
|
conversation.commands = commands;
|
|
@@ -509,11 +776,12 @@ export class Conversations {
|
|
|
509
776
|
options(conversation, options) {
|
|
510
777
|
if (conversation.running === null)
|
|
511
778
|
return;
|
|
779
|
+
conversation.options = options;
|
|
512
780
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
513
781
|
}
|
|
514
|
-
deliver(conversation, turn,
|
|
782
|
+
deliver(conversation, turn, act, body, current) {
|
|
515
783
|
this.reporting++;
|
|
516
|
-
const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit
|
|
784
|
+
const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/${act}`, { conversation: conversation.id, turn: turn.id, ...(await body) }, current));
|
|
517
785
|
conversation.reports = delivered
|
|
518
786
|
.catch(() => undefined)
|
|
519
787
|
.finally(() => {
|
|
@@ -523,7 +791,7 @@ export class Conversations {
|
|
|
523
791
|
return delivered;
|
|
524
792
|
}
|
|
525
793
|
write(conversation, turn, operation) {
|
|
526
|
-
if (
|
|
794
|
+
if (turn.ended || !this.watched)
|
|
527
795
|
return;
|
|
528
796
|
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
529
797
|
}
|
|
@@ -533,11 +801,12 @@ export class Conversations {
|
|
|
533
801
|
}
|
|
534
802
|
begin(conversation) {
|
|
535
803
|
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
536
|
-
conversation.
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
804
|
+
conversation.turns.add(turn);
|
|
805
|
+
conversation.running?.adapter.turn(true);
|
|
806
|
+
void this.deliver(conversation, turn, 'turns/start', {})
|
|
807
|
+
.then((answer) => {
|
|
808
|
+
if (answer.run === true)
|
|
809
|
+
turn.quietUntil = Date.now() + ACTIVITY_REPORT_MS;
|
|
541
810
|
})
|
|
542
811
|
.catch(logged);
|
|
543
812
|
conversation.turn = turn;
|
|
@@ -560,24 +829,30 @@ export class Conversations {
|
|
|
560
829
|
if (notice !== null)
|
|
561
830
|
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
562
831
|
turn.ended = true;
|
|
563
|
-
conversation.
|
|
832
|
+
conversation.turns.delete(turn);
|
|
833
|
+
conversation.running?.adapter.turn(conversation.turns.size > 0);
|
|
564
834
|
if (conversation.turn === turn)
|
|
565
835
|
conversation.turn = null;
|
|
836
|
+
const tokens = conversation.tokens;
|
|
837
|
+
conversation.tokens = null;
|
|
566
838
|
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
567
839
|
parts: turn.parts.saved(),
|
|
568
840
|
context: turn.parts.context,
|
|
841
|
+
...(tokens === null ? {} : { tokens }),
|
|
569
842
|
...(failed === null ? {} : { failed: failed.failure }),
|
|
570
843
|
}));
|
|
571
|
-
void this.deliver(conversation, turn, '
|
|
844
|
+
void this.deliver(conversation, turn, 'turns/end', body).catch(logged);
|
|
572
845
|
this.next(conversation);
|
|
573
846
|
this.kit.changed();
|
|
574
847
|
}
|
|
575
|
-
prompt(conversation, prompt) {
|
|
848
|
+
prompt(conversation, prompt, taken) {
|
|
576
849
|
const running = conversation.running;
|
|
577
850
|
const turn = this.begin(conversation);
|
|
578
851
|
turn.prompted = true;
|
|
852
|
+
turn.taken = taken;
|
|
579
853
|
conversation.prompted = running.sessionId;
|
|
580
854
|
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
855
|
+
this.took(turn);
|
|
581
856
|
const notice = answeredFailure(answer);
|
|
582
857
|
if (notice !== null) {
|
|
583
858
|
this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
|
|
@@ -590,8 +865,9 @@ export class Conversations {
|
|
|
590
865
|
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
591
866
|
}
|
|
592
867
|
else {
|
|
593
|
-
|
|
594
|
-
|
|
868
|
+
this.took(turn);
|
|
869
|
+
if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
|
|
870
|
+
running.adapter.retire();
|
|
595
871
|
this.detach(conversation, running);
|
|
596
872
|
}
|
|
597
873
|
this.end(conversation, turn, answeredWith(error));
|
|
@@ -599,21 +875,26 @@ export class Conversations {
|
|
|
599
875
|
});
|
|
600
876
|
}
|
|
601
877
|
started(conversation) {
|
|
602
|
-
|
|
878
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
879
|
+
this.took(turn);
|
|
880
|
+
return turn;
|
|
603
881
|
}
|
|
604
|
-
ended(conversation) {
|
|
882
|
+
ended(conversation, stop) {
|
|
605
883
|
const turn = conversation.turn;
|
|
606
884
|
if (turn === null)
|
|
607
885
|
return;
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
this.apply(conversation, turn, event);
|
|
614
|
-
conversation.turn = null;
|
|
615
|
-
this.kit.changed();
|
|
886
|
+
const held = stop !== null && !turn.steered && [...conversation.subagents.values()].some((subagent) => subagent.turn === turn);
|
|
887
|
+
if (!turn.prompted || held) {
|
|
888
|
+
this.took(turn);
|
|
889
|
+
this.end(conversation, turn, stop === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
890
|
+
return;
|
|
616
891
|
}
|
|
892
|
+
if (stop !== null)
|
|
893
|
+
return;
|
|
894
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
895
|
+
this.apply(conversation, turn, event);
|
|
896
|
+
conversation.turn = null;
|
|
897
|
+
this.kit.changed();
|
|
617
898
|
}
|
|
618
899
|
async ask(conversation, method, params, secret, signal) {
|
|
619
900
|
const turn = this.started(conversation);
|
|
@@ -621,7 +902,7 @@ export class Conversations {
|
|
|
621
902
|
const answered = new Promise((answer, fail) => {
|
|
622
903
|
this.questions.set(interactionId, { conversation, answer: (response) => answer(response) });
|
|
623
904
|
signal.addEventListener('abort', () => answer(null), { once: true });
|
|
624
|
-
this.deliver(conversation, turn, '
|
|
905
|
+
this.deliver(conversation, turn, 'conversations/interaction/ask', {
|
|
625
906
|
interaction_id: interactionId,
|
|
626
907
|
request: { method, params },
|
|
627
908
|
secret,
|
|
@@ -662,6 +943,18 @@ export class Conversations {
|
|
|
662
943
|
read(conversation, reader, message) {
|
|
663
944
|
for (const event of reader.read(message))
|
|
664
945
|
this.event(conversation, event);
|
|
946
|
+
this.reportActivity(conversation);
|
|
947
|
+
}
|
|
948
|
+
reportActivity(conversation) {
|
|
949
|
+
const turn = conversation.turn;
|
|
950
|
+
if (turn === null || Date.now() < turn.quietUntil)
|
|
951
|
+
return;
|
|
952
|
+
turn.quietUntil = Infinity;
|
|
953
|
+
void this.deliver(conversation, turn, 'turns/activity/report', {}, () => false)
|
|
954
|
+
.catch(logged)
|
|
955
|
+
.finally(() => {
|
|
956
|
+
turn.quietUntil = Date.now() + ACTIVITY_REPORT_MS;
|
|
957
|
+
});
|
|
665
958
|
}
|
|
666
959
|
event(conversation, event) {
|
|
667
960
|
switch (event.kind) {
|
|
@@ -669,7 +962,7 @@ export class Conversations {
|
|
|
669
962
|
this.started(conversation);
|
|
670
963
|
return;
|
|
671
964
|
case 'ended':
|
|
672
|
-
this.ended(conversation);
|
|
965
|
+
this.ended(conversation, event.stop);
|
|
673
966
|
return;
|
|
674
967
|
case 'commands':
|
|
675
968
|
this.commands(conversation, event.commands);
|
|
@@ -681,9 +974,15 @@ export class Conversations {
|
|
|
681
974
|
conversation.context = { used: event.used, window: event.window };
|
|
682
975
|
conversation.turn?.parts.measure(conversation.context);
|
|
683
976
|
return;
|
|
977
|
+
case 'tokens':
|
|
978
|
+
conversation.tokens = addedTokens(conversation.tokens, event.tokens);
|
|
979
|
+
return;
|
|
684
980
|
case 'subagent-done':
|
|
685
981
|
this.subagentDone(conversation, event.id, event.result);
|
|
686
982
|
return;
|
|
983
|
+
case 'subagent-activity':
|
|
984
|
+
this.subagentActivity(conversation, event.id, event.activity);
|
|
985
|
+
return;
|
|
687
986
|
case 'marker':
|
|
688
987
|
case 'image':
|
|
689
988
|
if (conversation.turn !== null)
|
|
@@ -694,6 +993,11 @@ export class Conversations {
|
|
|
694
993
|
}
|
|
695
994
|
this.apply(conversation, this.started(conversation), event);
|
|
696
995
|
}
|
|
996
|
+
took(turn) {
|
|
997
|
+
const taken = turn.taken;
|
|
998
|
+
turn.taken = null;
|
|
999
|
+
taken?.();
|
|
1000
|
+
}
|
|
697
1001
|
apply(conversation, turn, event) {
|
|
698
1002
|
switch (event.kind) {
|
|
699
1003
|
case 'text':
|
|
@@ -705,7 +1009,8 @@ export class Conversations {
|
|
|
705
1009
|
return;
|
|
706
1010
|
case 'subagent': {
|
|
707
1011
|
const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
|
|
708
|
-
conversation.subagents.set(event.id, { description: event.description, turn, index });
|
|
1012
|
+
conversation.subagents.set(event.id, { description: event.description, turn, index, activity: null });
|
|
1013
|
+
this.subagentsChanged(conversation);
|
|
709
1014
|
return;
|
|
710
1015
|
}
|
|
711
1016
|
case 'message':
|
|
@@ -728,6 +1033,7 @@ export class Conversations {
|
|
|
728
1033
|
if (subagent === undefined)
|
|
729
1034
|
return;
|
|
730
1035
|
conversation.subagents.delete(id);
|
|
1036
|
+
this.subagentsChanged(conversation);
|
|
731
1037
|
if (subagent.turn === conversation.turn) {
|
|
732
1038
|
subagent.turn.parts.done(subagent.index, result);
|
|
733
1039
|
return;
|
|
@@ -738,23 +1044,37 @@ export class Conversations {
|
|
|
738
1044
|
else
|
|
739
1045
|
conversation.turn.parts.start(part);
|
|
740
1046
|
}
|
|
1047
|
+
subagentActivity(conversation, id, activity) {
|
|
1048
|
+
const subagent = conversation.subagents.get(id);
|
|
1049
|
+
if (subagent === undefined || subagent.activity === activity)
|
|
1050
|
+
return;
|
|
1051
|
+
subagent.activity = activity;
|
|
1052
|
+
this.subagentsChanged(conversation);
|
|
1053
|
+
}
|
|
741
1054
|
async image(conversation, turn, reserved, data, mediaType) {
|
|
742
1055
|
const bytes = Buffer.from(data, 'base64');
|
|
743
1056
|
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
1057
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
744
1058
|
try {
|
|
745
|
-
const declared = (await this.deliver(conversation, turn, 'images', {
|
|
1059
|
+
const declared = (await this.deliver(conversation, turn, 'turns/images/declare', {
|
|
746
1060
|
name,
|
|
747
1061
|
media_type: mediaType,
|
|
748
1062
|
bytes: bytes.length,
|
|
749
|
-
sha256
|
|
1063
|
+
sha256,
|
|
750
1064
|
}));
|
|
751
|
-
const saved =
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1065
|
+
const saved = await uploaded(declared.upload, {
|
|
1066
|
+
name,
|
|
1067
|
+
bytes: bytes.length,
|
|
1068
|
+
sha256,
|
|
1069
|
+
read: (start, end) => [bytes.subarray(start, end)],
|
|
1070
|
+
send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
|
|
1071
|
+
finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
|
|
1072
|
+
});
|
|
1073
|
+
turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
|
|
755
1074
|
}
|
|
756
1075
|
catch (error) {
|
|
757
|
-
|
|
1076
|
+
const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
1077
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
|
|
758
1078
|
}
|
|
759
1079
|
}
|
|
760
1080
|
}
|