@agentshouse/kit 0.2.1-cb345f4157685 → 0.2.1-cb66cb2fd0696
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 +58 -7
- package/dist/agents.js +85 -40
- package/dist/api.js +5 -5
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +1 -1
- package/dist/building.js +50 -0
- package/dist/calls.js +71 -0
- package/dist/clis.js +34 -40
- package/dist/{stream.js → connection.js} +31 -49
- package/dist/conversations.js +423 -139
- package/dist/copies.js +36 -43
- package/dist/end.js +14 -4
- package/dist/events.js +29 -8
- package/dist/files.js +139 -37
- package/dist/git.js +3 -3
- 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/mcp.js +6 -2
- package/dist/placement.js +21 -20
- package/dist/relay.js +21 -0
- package/dist/resident.js +45 -38
- package/dist/scope.js +15 -4
- package/dist/scripts.js +255 -0
- package/dist/service.js +1 -1
- package/dist/sign-in.js +22 -21
- 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 +91 -125
- package/dist/wire.js +19 -3
- package/package.json +20 -21
- package/dist/bootstrap.json +0 -24
- package/dist/clis-main.js +0 -23
- package/dist/inputs.js +0 -37
- 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
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import { client,
|
|
1
|
+
import { client, } from '@agentclientprotocol/sdk';
|
|
2
2
|
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';
|
|
7
|
-
import { finalRefusal } from './api.js';
|
|
6
|
+
import { signedOut, startAdapter } from './acp.js';
|
|
7
|
+
import { finalRefusal, HouseRefusal } from './api.js';
|
|
8
8
|
import { openBridge } from './bridge.js';
|
|
9
9
|
import { CLIS } from './clis.js';
|
|
10
10
|
import { answeredFailure, readerFor } from './events.js';
|
|
11
|
-
import {
|
|
11
|
+
import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
|
|
12
12
|
import { agentBase } from './home.js';
|
|
13
13
|
import { instructions } from './instructions.js';
|
|
14
14
|
import { Parts } from './parts.js';
|
|
15
15
|
import { refusalOf } from './refusals.js';
|
|
16
|
+
import { relayed } from './relay.js';
|
|
16
17
|
import { killMarked, Scope } from './scope.js';
|
|
17
18
|
import { holdSecretInput } from './secret-input.js';
|
|
18
|
-
import { createHowWeWork } from './skills.js';
|
|
19
19
|
import { transcribe } from './transcription.js';
|
|
20
20
|
import { workFor } from './work.js';
|
|
21
|
-
const SIGNED_OUT = RequestError.authRequired().code;
|
|
22
21
|
const KILLED = 'the conversation was killed';
|
|
22
|
+
const INTERRUPTED = 'the conversation was interrupted';
|
|
23
23
|
const REFUSED = 'the CLI refused the prompt';
|
|
24
24
|
const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
|
|
25
25
|
const GONE = 'House no longer holds this conversation; start a new one';
|
|
@@ -31,9 +31,13 @@ const ENDED = {
|
|
|
31
31
|
const NO_START = "the Agent's CLI did not start";
|
|
32
32
|
const NO_SESSION = "the Agent's CLI did not open a session";
|
|
33
33
|
const NO_RESUME = "the Agent's CLI could not resume this conversation";
|
|
34
|
+
const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
|
|
34
35
|
const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
|
|
36
|
+
const UNSTEERED = "the Agent's CLI did not take the message into its running turn";
|
|
35
37
|
class NotStarted extends Error {
|
|
36
38
|
}
|
|
39
|
+
class ResumeRefused extends NotStarted {
|
|
40
|
+
}
|
|
37
41
|
async function step(reason, work) {
|
|
38
42
|
try {
|
|
39
43
|
return await work;
|
|
@@ -42,6 +46,11 @@ async function step(reason, work) {
|
|
|
42
46
|
throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
|
|
43
47
|
}
|
|
44
48
|
}
|
|
49
|
+
function unopened(reason) {
|
|
50
|
+
return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
|
|
51
|
+
}
|
|
52
|
+
// A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
|
|
53
|
+
const STARTING_SHOWN_AFTER_MS = 1_000;
|
|
45
54
|
function unbridged(error) {
|
|
46
55
|
return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
|
|
47
56
|
}
|
|
@@ -53,40 +62,57 @@ const EXIT_PLAN_MODE = {
|
|
|
53
62
|
return value;
|
|
54
63
|
},
|
|
55
64
|
};
|
|
56
|
-
export const
|
|
65
|
+
export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
|
|
57
66
|
function causeOf(error) {
|
|
58
67
|
return error instanceof Error ? error.message : String(error);
|
|
59
68
|
}
|
|
60
69
|
function answeredWith(error) {
|
|
61
|
-
|
|
62
|
-
return { failure: signedOut ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
70
|
+
return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
63
71
|
}
|
|
64
72
|
function exitedWith(running, cause) {
|
|
65
|
-
|
|
73
|
+
if (running.killed)
|
|
74
|
+
return { failure: 'killed', cause: KILLED };
|
|
75
|
+
return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
|
|
66
76
|
}
|
|
67
77
|
function answerMessage(request, response) {
|
|
68
78
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
69
79
|
}
|
|
80
|
+
async function sentPart(url, bytes, headers) {
|
|
81
|
+
const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
|
|
82
|
+
await answer.arrayBuffer();
|
|
83
|
+
if (!answer.ok)
|
|
84
|
+
throw storeRefusal('upload', answer.status);
|
|
85
|
+
}
|
|
70
86
|
function logged(error) {
|
|
71
87
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
72
88
|
}
|
|
73
89
|
function refusal(conversation, error) {
|
|
74
90
|
if (conversation.kills > 0)
|
|
75
91
|
return KILLED;
|
|
76
|
-
const cause = error instanceof
|
|
92
|
+
const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
|
|
77
93
|
logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
|
|
78
94
|
return causeOf(error);
|
|
79
95
|
}
|
|
96
|
+
function audioOf(result) {
|
|
97
|
+
if (result.isError === true)
|
|
98
|
+
return null;
|
|
99
|
+
const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
|
|
100
|
+
return file?.media_type?.startsWith('audio/') === true ? file : null;
|
|
101
|
+
}
|
|
80
102
|
function marker(bridge) {
|
|
81
103
|
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
82
104
|
}
|
|
105
|
+
function steers(adapter) {
|
|
106
|
+
return adapter.initialized._meta?.steering?.supported === true;
|
|
107
|
+
}
|
|
83
108
|
class Turn {
|
|
84
109
|
id = randomUUID();
|
|
85
110
|
parts;
|
|
86
|
-
admitted = false;
|
|
87
111
|
questions = 0;
|
|
88
112
|
secrets = 0;
|
|
89
113
|
prompted = false;
|
|
114
|
+
steered = false;
|
|
115
|
+
taken = null;
|
|
90
116
|
ended = false;
|
|
91
117
|
images = 0;
|
|
92
118
|
uploads = [];
|
|
@@ -100,21 +126,31 @@ class Conversation {
|
|
|
100
126
|
session = null;
|
|
101
127
|
turn = null;
|
|
102
128
|
commands = null;
|
|
129
|
+
options = null;
|
|
130
|
+
exitUntold = false;
|
|
103
131
|
context = null;
|
|
104
132
|
leftovers = new Set();
|
|
105
133
|
waiting = [];
|
|
134
|
+
done = false;
|
|
106
135
|
sending = null;
|
|
107
136
|
subagents = new Map();
|
|
108
137
|
held = [];
|
|
109
138
|
opening = null;
|
|
139
|
+
bridge = null;
|
|
140
|
+
launch = null;
|
|
110
141
|
prompted = null;
|
|
111
142
|
kills = 0;
|
|
112
143
|
killed = new AbortController();
|
|
113
144
|
interrupts = 0;
|
|
114
145
|
interrupted = new AbortController();
|
|
115
|
-
|
|
146
|
+
turns = new Set();
|
|
116
147
|
queue = Promise.resolve();
|
|
117
148
|
reports = Promise.resolve();
|
|
149
|
+
starting = null;
|
|
150
|
+
preparing = null;
|
|
151
|
+
told = null;
|
|
152
|
+
untold = false;
|
|
153
|
+
shown = null;
|
|
118
154
|
constructor(id) {
|
|
119
155
|
this.id = id;
|
|
120
156
|
}
|
|
@@ -138,23 +174,23 @@ export class Conversations {
|
|
|
138
174
|
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
139
175
|
setInterval(() => this.look(), 2_000);
|
|
140
176
|
}
|
|
141
|
-
|
|
142
|
-
void this.kit.
|
|
143
|
-
const conversation = this.conversation(
|
|
144
|
-
if (
|
|
177
|
+
call(call) {
|
|
178
|
+
void this.kit.calls.carry(call.input_id, () => {
|
|
179
|
+
const conversation = this.conversation(call.conversation_id);
|
|
180
|
+
if (call.kind === 'kill') {
|
|
145
181
|
conversation.kills++;
|
|
146
182
|
conversation.killed.abort(new Error(KILLED));
|
|
147
183
|
for (const waiting of conversation.waiting.splice(0))
|
|
148
184
|
waiting.settle({ refused: KILLED });
|
|
149
185
|
this.halt(conversation);
|
|
150
186
|
}
|
|
151
|
-
if (
|
|
187
|
+
if (call.kind === 'interrupt') {
|
|
152
188
|
conversation.interrupts++;
|
|
153
189
|
conversation.interrupted.abort();
|
|
154
190
|
}
|
|
155
|
-
const handled = conversation.queue.then(() => this.carry(conversation,
|
|
191
|
+
const handled = conversation.queue.then(() => this.carry(conversation, call));
|
|
156
192
|
conversation.queue = handled.catch(() => undefined);
|
|
157
|
-
return handled.then(async ({
|
|
193
|
+
return handled.then(async ({ result }) => ({ result: await result }));
|
|
158
194
|
});
|
|
159
195
|
}
|
|
160
196
|
idle() {
|
|
@@ -163,7 +199,7 @@ export class Conversations {
|
|
|
163
199
|
for (const conversation of this.conversations.values()) {
|
|
164
200
|
const turn = conversation.turn;
|
|
165
201
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
166
|
-
if (conversation.
|
|
202
|
+
if (conversation.turns.size > (asking ? 1 : 0))
|
|
167
203
|
return false;
|
|
168
204
|
}
|
|
169
205
|
const conversations = [...this.conversations.values()];
|
|
@@ -172,7 +208,7 @@ export class Conversations {
|
|
|
172
208
|
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
173
209
|
}
|
|
174
210
|
quiet() {
|
|
175
|
-
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.
|
|
211
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
|
|
176
212
|
}
|
|
177
213
|
look() {
|
|
178
214
|
for (const conversation of this.conversations.values()) {
|
|
@@ -187,6 +223,16 @@ export class Conversations {
|
|
|
187
223
|
}
|
|
188
224
|
opened() {
|
|
189
225
|
this.watched = false;
|
|
226
|
+
for (const conversation of this.conversations.values()) {
|
|
227
|
+
if (conversation.running !== null) {
|
|
228
|
+
this.facts(conversation);
|
|
229
|
+
this.subagentsChanged(conversation);
|
|
230
|
+
}
|
|
231
|
+
else if (conversation.exitUntold)
|
|
232
|
+
this.process(conversation, false);
|
|
233
|
+
if (conversation.untold || conversation.told !== null)
|
|
234
|
+
this.retell(conversation);
|
|
235
|
+
}
|
|
190
236
|
}
|
|
191
237
|
watching(watched) {
|
|
192
238
|
const resend = watched && !this.watched;
|
|
@@ -206,134 +252,320 @@ export class Conversations {
|
|
|
206
252
|
}
|
|
207
253
|
return conversation;
|
|
208
254
|
}
|
|
209
|
-
async carry(conversation,
|
|
210
|
-
if (
|
|
211
|
-
return
|
|
212
|
-
if (
|
|
213
|
-
return this.message(conversation,
|
|
214
|
-
if (
|
|
255
|
+
async carry(conversation, call) {
|
|
256
|
+
if (call.kind === 'open')
|
|
257
|
+
return this.open(conversation, call);
|
|
258
|
+
if (call.kind === 'message')
|
|
259
|
+
return this.message(conversation, call);
|
|
260
|
+
if (call.kind === 'done') {
|
|
261
|
+
conversation.done = true;
|
|
262
|
+
this.next(conversation);
|
|
263
|
+
return { result: Promise.resolve({}) };
|
|
264
|
+
}
|
|
265
|
+
await this.launched(conversation);
|
|
266
|
+
if (call.kind === 'interrupt') {
|
|
215
267
|
if (--conversation.interrupts === 0)
|
|
216
268
|
conversation.interrupted = new AbortController();
|
|
217
|
-
await this.interrupt(conversation, String(
|
|
269
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
218
270
|
}
|
|
219
|
-
if (
|
|
271
|
+
if (call.kind === 'kill') {
|
|
220
272
|
await this.kill(conversation);
|
|
221
273
|
await conversation.sending;
|
|
222
274
|
if (--conversation.kills === 0)
|
|
223
275
|
conversation.killed = new AbortController();
|
|
224
276
|
}
|
|
225
|
-
if (
|
|
226
|
-
await this.option(conversation, String(
|
|
227
|
-
if (
|
|
228
|
-
const question = this.questions.get(String(
|
|
277
|
+
if (call.kind === 'option')
|
|
278
|
+
await this.option(conversation, String(call.option), call.value);
|
|
279
|
+
if (call.kind === 'answer') {
|
|
280
|
+
const question = this.questions.get(String(call.interaction_id));
|
|
229
281
|
if (question === undefined) {
|
|
230
|
-
const text = answerMessage(
|
|
231
|
-
return this.message(conversation, { ...
|
|
282
|
+
const text = answerMessage(call.request, call.response);
|
|
283
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
232
284
|
}
|
|
233
|
-
question.answer(
|
|
285
|
+
question.answer(call.response);
|
|
234
286
|
}
|
|
235
|
-
return {
|
|
287
|
+
return { result: Promise.resolve({}) };
|
|
288
|
+
}
|
|
289
|
+
async open(conversation, call) {
|
|
290
|
+
await this.stop(conversation);
|
|
291
|
+
const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
|
|
292
|
+
return {
|
|
293
|
+
result: launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) })),
|
|
294
|
+
};
|
|
236
295
|
}
|
|
237
|
-
async
|
|
238
|
-
|
|
239
|
-
|
|
296
|
+
async stop(conversation) {
|
|
297
|
+
const running = conversation.running;
|
|
298
|
+
running?.scope.kill();
|
|
299
|
+
await running?.adapter.exited;
|
|
300
|
+
}
|
|
301
|
+
launch(conversation, route, sessionId, empty) {
|
|
302
|
+
let opened;
|
|
303
|
+
let failed;
|
|
304
|
+
const bridge = new Promise((resolve, reject) => {
|
|
305
|
+
opened = resolve;
|
|
306
|
+
failed = reject;
|
|
307
|
+
});
|
|
308
|
+
bridge.catch(() => undefined);
|
|
309
|
+
const session = this.start(conversation, route, sessionId, empty, opened);
|
|
310
|
+
session.catch(failed);
|
|
311
|
+
const launch = { bridge, session };
|
|
312
|
+
conversation.launch = launch;
|
|
313
|
+
void session
|
|
314
|
+
.catch(() => undefined)
|
|
315
|
+
.then(() => {
|
|
316
|
+
if (conversation.launch !== launch)
|
|
317
|
+
return;
|
|
318
|
+
conversation.launch = null;
|
|
319
|
+
this.closeIdleBridge(conversation);
|
|
320
|
+
this.next(conversation);
|
|
321
|
+
});
|
|
322
|
+
this.next(conversation);
|
|
323
|
+
return launch;
|
|
324
|
+
}
|
|
325
|
+
async launched(conversation) {
|
|
326
|
+
while (conversation.launch !== null)
|
|
327
|
+
await conversation.launch.session.catch(() => undefined);
|
|
328
|
+
}
|
|
329
|
+
instructed(conversation, route) {
|
|
330
|
+
const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
|
|
331
|
+
if (bridge === undefined)
|
|
332
|
+
return null;
|
|
333
|
+
const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
|
|
334
|
+
read.catch(() => undefined);
|
|
335
|
+
return read;
|
|
336
|
+
}
|
|
337
|
+
async message(conversation, call) {
|
|
338
|
+
const content = [
|
|
339
|
+
...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
|
|
340
|
+
{ type: 'text', text: String(call.text) },
|
|
341
|
+
];
|
|
342
|
+
let route;
|
|
343
|
+
let instructing = null;
|
|
240
344
|
try {
|
|
241
|
-
|
|
345
|
+
route = await this.route(conversation, call.agent_id);
|
|
346
|
+
if (call.first === true)
|
|
347
|
+
instructing = this.instructed(conversation, route);
|
|
348
|
+
if (call.message_id !== null)
|
|
349
|
+
content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
|
|
350
|
+
if (conversation.kills > 0)
|
|
351
|
+
throw new Error(KILLED);
|
|
242
352
|
}
|
|
243
353
|
catch (error) {
|
|
244
|
-
|
|
354
|
+
this.closeIdleBridge(conversation);
|
|
355
|
+
this.prepares(conversation, null);
|
|
356
|
+
return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
245
357
|
}
|
|
358
|
+
conversation.preparing = null;
|
|
359
|
+
const { promise, resolve: settle } = Promise.withResolvers();
|
|
360
|
+
const waiting = { call, route, content, instructing, settle };
|
|
361
|
+
if (call.at_once === true && conversation.turns.size > 0)
|
|
362
|
+
await this.atOnce(conversation, waiting);
|
|
363
|
+
else
|
|
364
|
+
conversation.waiting.push(waiting);
|
|
365
|
+
this.next(conversation);
|
|
366
|
+
return { result: promise };
|
|
246
367
|
}
|
|
247
|
-
async
|
|
248
|
-
|
|
249
|
-
|
|
368
|
+
async atOnce(conversation, waiting) {
|
|
369
|
+
while (conversation.sending !== null)
|
|
370
|
+
await conversation.sending;
|
|
371
|
+
const running = conversation.running;
|
|
372
|
+
if (running !== null && conversation.turns.size > 0 && steers(running.adapter)) {
|
|
373
|
+
conversation.sending = this.steer(conversation, running, waiting);
|
|
374
|
+
await conversation.sending;
|
|
375
|
+
conversation.sending = null;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
this.ahead(conversation, waiting);
|
|
379
|
+
if (conversation.turns.size > 0)
|
|
380
|
+
await this.cancel(conversation);
|
|
381
|
+
}
|
|
382
|
+
ahead(conversation, waiting) {
|
|
383
|
+
const behind = conversation.waiting.findIndex((held) => held.call.at_once !== true);
|
|
384
|
+
conversation.waiting.splice(behind === -1 ? conversation.waiting.length : behind, 0, waiting);
|
|
385
|
+
}
|
|
386
|
+
async steer(conversation, running, waiting) {
|
|
387
|
+
let outcome;
|
|
250
388
|
try {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
content.push({ type: 'text', text: lines.join('\n') });
|
|
257
|
-
if (conversation.kills > 0)
|
|
258
|
-
throw new Error(KILLED);
|
|
389
|
+
({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
|
|
390
|
+
sessionId: running.sessionId,
|
|
391
|
+
prompt: waiting.content,
|
|
392
|
+
_meta: { steering: { idleBehavior: 'promptRequired' } },
|
|
393
|
+
}));
|
|
259
394
|
}
|
|
260
395
|
catch (error) {
|
|
261
|
-
|
|
396
|
+
outcome = error;
|
|
397
|
+
}
|
|
398
|
+
if (outcome === 'promptRequired') {
|
|
399
|
+
this.ahead(conversation, waiting);
|
|
400
|
+
}
|
|
401
|
+
else if (outcome === 'injected' || outcome === 'startedNewTurn') {
|
|
402
|
+
if (outcome === 'injected') {
|
|
403
|
+
for (const turn of conversation.turns)
|
|
404
|
+
turn.steered ||= turn.prompted;
|
|
405
|
+
}
|
|
406
|
+
this.kit.calls.keepResult(String(waiting.call.input_id), {});
|
|
407
|
+
waiting.settle({});
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const cause = outcome instanceof Error ? outcome : new Error(`the CLI answered ${JSON.stringify(outcome)}`);
|
|
411
|
+
waiting.settle({ refused: refusal(conversation, new NotStarted(UNSTEERED, { cause })) });
|
|
262
412
|
}
|
|
263
|
-
return {
|
|
264
|
-
ack: new Promise((settle) => {
|
|
265
|
-
conversation.waiting.push({ input, route, content, settle });
|
|
266
|
-
this.next(conversation);
|
|
267
|
-
}),
|
|
268
|
-
};
|
|
269
413
|
}
|
|
270
414
|
next(conversation) {
|
|
271
|
-
if (conversation.sending !== null || conversation.
|
|
415
|
+
if (conversation.sending !== null || conversation.turns.size > 0)
|
|
272
416
|
return;
|
|
273
417
|
const waiting = conversation.waiting.shift();
|
|
274
|
-
if (waiting === undefined)
|
|
418
|
+
if (waiting === undefined) {
|
|
419
|
+
const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
|
|
420
|
+
if (conversation.starting !== step)
|
|
421
|
+
this.starts(conversation, step);
|
|
422
|
+
if (conversation.done && conversation.launch === null)
|
|
423
|
+
void this.stop(conversation).catch(logged);
|
|
275
424
|
return;
|
|
425
|
+
}
|
|
276
426
|
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
277
427
|
conversation.sending = null;
|
|
278
428
|
this.next(conversation);
|
|
279
429
|
});
|
|
280
430
|
}
|
|
281
431
|
async send(conversation, waiting) {
|
|
282
|
-
const {
|
|
283
|
-
let
|
|
432
|
+
const { call } = waiting;
|
|
433
|
+
let result = {};
|
|
284
434
|
let started = false;
|
|
435
|
+
let instructing = waiting.instructing;
|
|
285
436
|
const prompt = [];
|
|
286
437
|
try {
|
|
287
438
|
if (conversation.running === null) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
439
|
+
this.starts(conversation, 'session');
|
|
440
|
+
await this.launched(conversation);
|
|
441
|
+
}
|
|
442
|
+
if (conversation.running === null) {
|
|
443
|
+
const stored = conversation.session ?? call.provider_session_id;
|
|
444
|
+
const empty = call.first === true && conversation.prompted !== stored;
|
|
445
|
+
const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
|
|
446
|
+
if (call.first === true)
|
|
447
|
+
instructing = this.instructed(conversation, waiting.route);
|
|
448
|
+
started = (await launch.session) !== stored;
|
|
291
449
|
}
|
|
292
450
|
const session = conversation.running.sessionId;
|
|
293
|
-
if (started || (
|
|
294
|
-
|
|
451
|
+
if (started || (call.first === true && session !== call.provider_session_id)) {
|
|
452
|
+
result = { provider_session_id: session };
|
|
295
453
|
}
|
|
296
|
-
if (
|
|
297
|
-
prompt.push({
|
|
298
|
-
type: 'text',
|
|
299
|
-
text: await instructions(conversation.running.bridge, waiting.route.base_instructions, await this.kit.copies.mapping()),
|
|
300
|
-
});
|
|
454
|
+
if (call.first === true) {
|
|
455
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
301
456
|
}
|
|
302
457
|
if (conversation.kills > 0)
|
|
303
458
|
throw new Error(KILLED);
|
|
304
459
|
}
|
|
305
460
|
catch (error) {
|
|
461
|
+
this.starts(conversation, null);
|
|
306
462
|
if (started) {
|
|
307
463
|
conversation.session = null;
|
|
308
464
|
await this.kill(conversation);
|
|
309
465
|
}
|
|
310
|
-
|
|
466
|
+
const refused = refusal(conversation, error);
|
|
467
|
+
const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
|
|
468
|
+
waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
|
|
311
469
|
return;
|
|
312
470
|
}
|
|
313
|
-
this.
|
|
314
|
-
waiting.
|
|
471
|
+
this.starts(conversation, null);
|
|
472
|
+
this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
|
|
473
|
+
waiting.settle(result);
|
|
315
474
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
475
|
+
starts(conversation, step) {
|
|
476
|
+
conversation.starting = step;
|
|
477
|
+
if (conversation.shown !== null)
|
|
478
|
+
clearTimeout(conversation.shown);
|
|
479
|
+
conversation.shown = null;
|
|
480
|
+
if (step === null || conversation.told !== null) {
|
|
481
|
+
this.tell(conversation, step);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
conversation.shown = setTimeout(() => {
|
|
485
|
+
conversation.shown = null;
|
|
486
|
+
if (conversation.starting === step)
|
|
487
|
+
this.tell(conversation, step);
|
|
488
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
489
|
+
}
|
|
490
|
+
prepares(conversation, step) {
|
|
491
|
+
conversation.preparing = step;
|
|
492
|
+
this.next(conversation);
|
|
493
|
+
}
|
|
494
|
+
tell(conversation, step) {
|
|
495
|
+
if (conversation.told === step && !conversation.untold)
|
|
496
|
+
return;
|
|
497
|
+
conversation.told = step;
|
|
498
|
+
this.retell(conversation);
|
|
499
|
+
}
|
|
500
|
+
retell(conversation) {
|
|
501
|
+
conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
|
|
502
|
+
}
|
|
503
|
+
async transcribed(conversation, messageId, text) {
|
|
504
|
+
const paths = linkedPaths(text);
|
|
505
|
+
if (paths.length === 0)
|
|
506
|
+
return [];
|
|
507
|
+
const killed = conversation.killed.signal;
|
|
508
|
+
const interrupted = conversation.interrupted.signal;
|
|
509
|
+
const transcribing = AbortSignal.any([killed, interrupted]);
|
|
510
|
+
let signal = killed;
|
|
511
|
+
const blocks = [];
|
|
512
|
+
try {
|
|
513
|
+
const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
|
|
514
|
+
const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
|
|
515
|
+
for (const path of paths) {
|
|
516
|
+
let answered = await read(path);
|
|
517
|
+
if (answered === null)
|
|
518
|
+
continue;
|
|
519
|
+
signal = transcribing;
|
|
520
|
+
this.prepares(conversation, 'transcription');
|
|
521
|
+
const said = await transcribe(async () => {
|
|
522
|
+
const address = answered ?? (await read(path));
|
|
523
|
+
answered = null;
|
|
524
|
+
if (address === null)
|
|
525
|
+
throw new Error(`${path} is no longer an audio file`);
|
|
526
|
+
return address;
|
|
527
|
+
}, signal);
|
|
528
|
+
const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
|
|
529
|
+
await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
|
|
530
|
+
blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
|
|
331
531
|
}
|
|
332
532
|
}
|
|
333
|
-
|
|
533
|
+
catch (error) {
|
|
534
|
+
if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
|
|
535
|
+
throw new NotStarted(INTERRUPTED, { cause: error });
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
return blocks;
|
|
539
|
+
}
|
|
540
|
+
bridged(conversation) {
|
|
541
|
+
if (conversation.bridge === null) {
|
|
542
|
+
const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
543
|
+
conversation.bridge = bridging;
|
|
544
|
+
bridging.catch(() => {
|
|
545
|
+
if (conversation.bridge === bridging)
|
|
546
|
+
conversation.bridge = null;
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
return conversation.bridge;
|
|
550
|
+
}
|
|
551
|
+
closeBridge(conversation, bridging, bridge) {
|
|
552
|
+
bridge.close();
|
|
553
|
+
if (conversation.bridge === bridging)
|
|
554
|
+
conversation.bridge = null;
|
|
555
|
+
}
|
|
556
|
+
closeIdleBridge(conversation) {
|
|
557
|
+
const bridging = conversation.bridge;
|
|
558
|
+
if (bridging === null || conversation.running !== null || conversation.launch !== null)
|
|
559
|
+
return;
|
|
560
|
+
conversation.bridge = null;
|
|
561
|
+
void bridging.then((bridge) => bridge.close(), () => undefined);
|
|
334
562
|
}
|
|
335
563
|
async interrupt(conversation, turnId) {
|
|
336
|
-
if (conversation.
|
|
564
|
+
if ([...conversation.turns].some((turn) => turn.id === turnId))
|
|
565
|
+
await this.cancel(conversation);
|
|
566
|
+
}
|
|
567
|
+
async cancel(conversation) {
|
|
568
|
+
if (conversation.running === null)
|
|
337
569
|
return;
|
|
338
570
|
await conversation.running.adapter.connection.agent.notify('session/cancel', {
|
|
339
571
|
sessionId: conversation.running.sessionId,
|
|
@@ -385,7 +617,8 @@ export class Conversations {
|
|
|
385
617
|
}
|
|
386
618
|
return route;
|
|
387
619
|
}
|
|
388
|
-
async start(conversation,
|
|
620
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
621
|
+
const route = await routed;
|
|
389
622
|
const reader = readerFor(route.kind);
|
|
390
623
|
reader.session = sessionId;
|
|
391
624
|
const app = client({ name: '@agentshouse/kit' })
|
|
@@ -393,9 +626,10 @@ export class Conversations {
|
|
|
393
626
|
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
394
627
|
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
|
|
395
628
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
396
|
-
const
|
|
629
|
+
const bridging = this.bridged(conversation);
|
|
630
|
+
const bridge = await step(unbridged, bridging);
|
|
397
631
|
if (conversation.kills > 0) {
|
|
398
|
-
|
|
632
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
399
633
|
throw new Error(KILLED);
|
|
400
634
|
}
|
|
401
635
|
conversation.opening = bridge;
|
|
@@ -403,33 +637,32 @@ export class Conversations {
|
|
|
403
637
|
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
404
638
|
let adapter = null;
|
|
405
639
|
try {
|
|
406
|
-
|
|
640
|
+
bridged(bridge);
|
|
407
641
|
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
408
642
|
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
409
643
|
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
410
644
|
work.read(message);
|
|
411
645
|
this.read(conversation, reader, message);
|
|
412
646
|
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
413
|
-
void adapter.exited.then(() =>
|
|
647
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
414
648
|
const agent = adapter.connection.agent;
|
|
415
649
|
const cwd = route.working_directory;
|
|
416
650
|
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
417
651
|
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
418
|
-
const opened = resumed ?? (await step(NO_SESSION, agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
652
|
+
const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
419
653
|
reader.session = opened.sessionId;
|
|
420
654
|
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
421
655
|
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
422
|
-
await step(NO_SESSION, agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
656
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
423
657
|
}
|
|
424
|
-
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
658
|
+
const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
425
659
|
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
426
660
|
void work.settled().then(() => scope.record());
|
|
427
661
|
conversation.running = running;
|
|
662
|
+
adapter.turn(conversation.turns.size > 0);
|
|
428
663
|
conversation.session = running.sessionId;
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
this.commands(conversation, conversation.commands);
|
|
432
|
-
this.options(conversation, options);
|
|
664
|
+
conversation.options = options;
|
|
665
|
+
this.facts(conversation);
|
|
433
666
|
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
434
667
|
return running.sessionId;
|
|
435
668
|
}
|
|
@@ -437,7 +670,7 @@ export class Conversations {
|
|
|
437
670
|
adapter?.retire();
|
|
438
671
|
work.close();
|
|
439
672
|
conversation.leftovers.add(scope);
|
|
440
|
-
|
|
673
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
441
674
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
442
675
|
}
|
|
443
676
|
finally {
|
|
@@ -450,7 +683,7 @@ export class Conversations {
|
|
|
450
683
|
}
|
|
451
684
|
catch (error) {
|
|
452
685
|
if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
|
|
453
|
-
throw new NotStarted(NO_RESUME, { cause: error });
|
|
686
|
+
throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
|
|
454
687
|
}
|
|
455
688
|
logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
|
|
456
689
|
return null;
|
|
@@ -486,15 +719,36 @@ export class Conversations {
|
|
|
486
719
|
this.kit.changed();
|
|
487
720
|
}
|
|
488
721
|
detach(conversation, running) {
|
|
722
|
+
if (conversation.bridge === running.bridging)
|
|
723
|
+
conversation.bridge = null;
|
|
489
724
|
running.work.close();
|
|
490
725
|
conversation.leftovers.add(running.scope);
|
|
491
726
|
conversation.running = null;
|
|
492
727
|
conversation.commands = null;
|
|
728
|
+
conversation.options = null;
|
|
729
|
+
if (conversation.subagents.size > 0) {
|
|
730
|
+
conversation.subagents.clear();
|
|
731
|
+
this.subagentsChanged(conversation);
|
|
732
|
+
}
|
|
493
733
|
for (const [id, question] of this.questions) {
|
|
494
734
|
if (question.conversation === conversation)
|
|
495
735
|
this.questions.delete(id);
|
|
496
736
|
}
|
|
497
|
-
this.
|
|
737
|
+
this.process(conversation, false);
|
|
738
|
+
}
|
|
739
|
+
process(conversation, running) {
|
|
740
|
+
conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
|
|
741
|
+
}
|
|
742
|
+
facts(conversation) {
|
|
743
|
+
this.process(conversation, true);
|
|
744
|
+
if (conversation.commands !== null)
|
|
745
|
+
this.commands(conversation, conversation.commands);
|
|
746
|
+
if (conversation.options !== null)
|
|
747
|
+
this.options(conversation, conversation.options);
|
|
748
|
+
}
|
|
749
|
+
subagentsChanged(conversation) {
|
|
750
|
+
const subagents = [...conversation.subagents.values()].map(({ description, activity }) => ({ description, activity }));
|
|
751
|
+
this.kit.send({ type: 'subagents', conversation_id: conversation.id, subagents });
|
|
498
752
|
}
|
|
499
753
|
commands(conversation, commands) {
|
|
500
754
|
conversation.commands = commands;
|
|
@@ -505,6 +759,7 @@ export class Conversations {
|
|
|
505
759
|
options(conversation, options) {
|
|
506
760
|
if (conversation.running === null)
|
|
507
761
|
return;
|
|
762
|
+
conversation.options = options;
|
|
508
763
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
509
764
|
}
|
|
510
765
|
deliver(conversation, turn, event, body) {
|
|
@@ -519,7 +774,7 @@ export class Conversations {
|
|
|
519
774
|
return delivered;
|
|
520
775
|
}
|
|
521
776
|
write(conversation, turn, operation) {
|
|
522
|
-
if (
|
|
777
|
+
if (turn.ended || !this.watched)
|
|
523
778
|
return;
|
|
524
779
|
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
525
780
|
}
|
|
@@ -529,13 +784,9 @@ export class Conversations {
|
|
|
529
784
|
}
|
|
530
785
|
begin(conversation) {
|
|
531
786
|
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
532
|
-
conversation.
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
turn.admitted = true;
|
|
536
|
-
this.replay(conversation, turn);
|
|
537
|
-
})
|
|
538
|
-
.catch(logged);
|
|
787
|
+
conversation.turns.add(turn);
|
|
788
|
+
conversation.running?.adapter.turn(true);
|
|
789
|
+
void this.deliver(conversation, turn, 'started', {}).catch(logged);
|
|
539
790
|
conversation.turn = turn;
|
|
540
791
|
if (conversation.context !== null)
|
|
541
792
|
turn.parts.measure(conversation.context);
|
|
@@ -556,7 +807,8 @@ export class Conversations {
|
|
|
556
807
|
if (notice !== null)
|
|
557
808
|
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
558
809
|
turn.ended = true;
|
|
559
|
-
conversation.
|
|
810
|
+
conversation.turns.delete(turn);
|
|
811
|
+
conversation.running?.adapter.turn(conversation.turns.size > 0);
|
|
560
812
|
if (conversation.turn === turn)
|
|
561
813
|
conversation.turn = null;
|
|
562
814
|
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
@@ -568,12 +820,14 @@ export class Conversations {
|
|
|
568
820
|
this.next(conversation);
|
|
569
821
|
this.kit.changed();
|
|
570
822
|
}
|
|
571
|
-
prompt(conversation, prompt) {
|
|
823
|
+
prompt(conversation, prompt, taken) {
|
|
572
824
|
const running = conversation.running;
|
|
573
825
|
const turn = this.begin(conversation);
|
|
574
826
|
turn.prompted = true;
|
|
827
|
+
turn.taken = taken;
|
|
575
828
|
conversation.prompted = running.sessionId;
|
|
576
829
|
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
830
|
+
this.took(turn);
|
|
577
831
|
const notice = answeredFailure(answer);
|
|
578
832
|
if (notice !== null) {
|
|
579
833
|
this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
|
|
@@ -586,7 +840,8 @@ export class Conversations {
|
|
|
586
840
|
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
587
841
|
}
|
|
588
842
|
else {
|
|
589
|
-
|
|
843
|
+
this.took(turn);
|
|
844
|
+
if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
|
|
590
845
|
running.adapter.retire();
|
|
591
846
|
this.detach(conversation, running);
|
|
592
847
|
}
|
|
@@ -595,21 +850,26 @@ export class Conversations {
|
|
|
595
850
|
});
|
|
596
851
|
}
|
|
597
852
|
started(conversation) {
|
|
598
|
-
|
|
853
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
854
|
+
this.took(turn);
|
|
855
|
+
return turn;
|
|
599
856
|
}
|
|
600
|
-
ended(conversation) {
|
|
857
|
+
ended(conversation, stop) {
|
|
601
858
|
const turn = conversation.turn;
|
|
602
859
|
if (turn === null)
|
|
603
860
|
return;
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
this.apply(conversation, turn, event);
|
|
610
|
-
conversation.turn = null;
|
|
611
|
-
this.kit.changed();
|
|
861
|
+
const held = stop !== null && !turn.steered && [...conversation.subagents.values()].some((subagent) => subagent.turn === turn);
|
|
862
|
+
if (!turn.prompted || held) {
|
|
863
|
+
this.took(turn);
|
|
864
|
+
this.end(conversation, turn, stop === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
865
|
+
return;
|
|
612
866
|
}
|
|
867
|
+
if (stop !== null)
|
|
868
|
+
return;
|
|
869
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
870
|
+
this.apply(conversation, turn, event);
|
|
871
|
+
conversation.turn = null;
|
|
872
|
+
this.kit.changed();
|
|
613
873
|
}
|
|
614
874
|
async ask(conversation, method, params, secret, signal) {
|
|
615
875
|
const turn = this.started(conversation);
|
|
@@ -665,7 +925,7 @@ export class Conversations {
|
|
|
665
925
|
this.started(conversation);
|
|
666
926
|
return;
|
|
667
927
|
case 'ended':
|
|
668
|
-
this.ended(conversation);
|
|
928
|
+
this.ended(conversation, event.stop);
|
|
669
929
|
return;
|
|
670
930
|
case 'commands':
|
|
671
931
|
this.commands(conversation, event.commands);
|
|
@@ -680,6 +940,9 @@ export class Conversations {
|
|
|
680
940
|
case 'subagent-done':
|
|
681
941
|
this.subagentDone(conversation, event.id, event.result);
|
|
682
942
|
return;
|
|
943
|
+
case 'subagent-activity':
|
|
944
|
+
this.subagentActivity(conversation, event.id, event.activity);
|
|
945
|
+
return;
|
|
683
946
|
case 'marker':
|
|
684
947
|
case 'image':
|
|
685
948
|
if (conversation.turn !== null)
|
|
@@ -690,6 +953,11 @@ export class Conversations {
|
|
|
690
953
|
}
|
|
691
954
|
this.apply(conversation, this.started(conversation), event);
|
|
692
955
|
}
|
|
956
|
+
took(turn) {
|
|
957
|
+
const taken = turn.taken;
|
|
958
|
+
turn.taken = null;
|
|
959
|
+
taken?.();
|
|
960
|
+
}
|
|
693
961
|
apply(conversation, turn, event) {
|
|
694
962
|
switch (event.kind) {
|
|
695
963
|
case 'text':
|
|
@@ -701,7 +969,8 @@ export class Conversations {
|
|
|
701
969
|
return;
|
|
702
970
|
case 'subagent': {
|
|
703
971
|
const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
|
|
704
|
-
conversation.subagents.set(event.id, { description: event.description, turn, index });
|
|
972
|
+
conversation.subagents.set(event.id, { description: event.description, turn, index, activity: null });
|
|
973
|
+
this.subagentsChanged(conversation);
|
|
705
974
|
return;
|
|
706
975
|
}
|
|
707
976
|
case 'message':
|
|
@@ -724,6 +993,7 @@ export class Conversations {
|
|
|
724
993
|
if (subagent === undefined)
|
|
725
994
|
return;
|
|
726
995
|
conversation.subagents.delete(id);
|
|
996
|
+
this.subagentsChanged(conversation);
|
|
727
997
|
if (subagent.turn === conversation.turn) {
|
|
728
998
|
subagent.turn.parts.done(subagent.index, result);
|
|
729
999
|
return;
|
|
@@ -734,23 +1004,37 @@ export class Conversations {
|
|
|
734
1004
|
else
|
|
735
1005
|
conversation.turn.parts.start(part);
|
|
736
1006
|
}
|
|
1007
|
+
subagentActivity(conversation, id, activity) {
|
|
1008
|
+
const subagent = conversation.subagents.get(id);
|
|
1009
|
+
if (subagent === undefined || subagent.activity === activity)
|
|
1010
|
+
return;
|
|
1011
|
+
subagent.activity = activity;
|
|
1012
|
+
this.subagentsChanged(conversation);
|
|
1013
|
+
}
|
|
737
1014
|
async image(conversation, turn, reserved, data, mediaType) {
|
|
738
1015
|
const bytes = Buffer.from(data, 'base64');
|
|
739
1016
|
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
1017
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
740
1018
|
try {
|
|
741
1019
|
const declared = (await this.deliver(conversation, turn, 'images', {
|
|
742
1020
|
name,
|
|
743
1021
|
media_type: mediaType,
|
|
744
1022
|
bytes: bytes.length,
|
|
745
|
-
sha256
|
|
1023
|
+
sha256,
|
|
746
1024
|
}));
|
|
747
|
-
const saved =
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
1025
|
+
const saved = await uploaded(declared.upload, {
|
|
1026
|
+
name,
|
|
1027
|
+
bytes: bytes.length,
|
|
1028
|
+
sha256,
|
|
1029
|
+
read: (start, end) => [bytes.subarray(start, end)],
|
|
1030
|
+
send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
|
|
1031
|
+
finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
|
|
1032
|
+
});
|
|
1033
|
+
turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
|
|
751
1034
|
}
|
|
752
1035
|
catch (error) {
|
|
753
|
-
|
|
1036
|
+
const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
1037
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
|
|
754
1038
|
}
|
|
755
1039
|
}
|
|
756
1040
|
}
|