@agentshouse/kit 0.2.1-c53a84236c3d9 → 0.2.1-c57b73ff93a76
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 +44 -43
- package/dist/acp.js +90 -19
- package/dist/agents.js +118 -40
- package/dist/api.js +7 -3
- package/dist/app-git.js +10 -26
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +4 -7
- package/dist/building.js +46 -0
- package/dist/calls.js +71 -0
- package/dist/chatgpt.js +32 -0
- package/dist/clis-main.js +2 -2
- package/dist/clis.js +92 -23
- package/dist/connection.js +116 -0
- package/dist/conversations.js +531 -177
- package/dist/copies.js +91 -54
- package/dist/end.js +29 -4
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +33 -8
- package/dist/files.js +139 -31
- package/dist/git.js +13 -11
- package/dist/home.js +96 -7
- package/dist/house-cli.js +299 -0
- package/dist/house-main.js +7 -188
- package/dist/instructions.js +7 -10
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -22
- package/dist/login.js +113 -23
- package/dist/logout.js +0 -2
- package/dist/mcp.js +6 -2
- package/dist/parts.js +34 -16
- package/dist/placement.js +39 -0
- package/dist/relay.js +33 -2
- package/dist/resident.js +99 -34
- package/dist/scope.js +3 -0
- package/dist/scripts.js +255 -0
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/sign-in.js +22 -39
- package/dist/silence.js +28 -0
- package/dist/skills.js +49 -35
- package/dist/transcription.js +85 -45
- package/dist/translate.js +2 -8
- package/dist/update.js +115 -53
- package/dist/wire.js +22 -0
- package/dist/work.js +3 -5
- package/package.json +20 -20
- package/dist/bootstrap.json +0 -24
- package/dist/stream.js +0 -72
- package/how-we-work.md +0 -23
- package/skills/ask-matt/LICENSE +0 -21
- package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
- package/skills/ask-matt/SKILL.md +0 -91
- package/skills/ask-matt/agents/openai.yaml +0 -4
- 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 -116
- package/skills/codebase-design/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/LICENSE +0 -21
- package/skills/diagnosing-bugs/SKILL.md +0 -140
- 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/CONTEXT-FORMAT.md +0 -60
- package/skills/domain-modeling/LICENSE +0 -21
- package/skills/domain-modeling/SKILL.md +0 -76
- package/skills/domain-modeling/agents/openai.yaml +0 -3
- package/skills/grill-me/LICENSE +0 -21
- package/skills/grill-me/SKILL.md +0 -8
- package/skills/grill-me/agents/openai.yaml +0 -4
- package/skills/grill-with-docs/LICENSE +0 -21
- package/skills/grill-with-docs/SKILL.md +0 -8
- package/skills/grill-with-docs/agents/openai.yaml +0 -4
- 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 -17
- package/skills/handoff/agents/openai.yaml +0 -4
- package/skills/implement/LICENSE +0 -21
- package/skills/implement/SKILL.md +0 -16
- package/skills/implement/agents/openai.yaml +0 -4
- 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 -72
- package/skills/improve-codebase-architecture/agents/openai.yaml +0 -4
- package/skills/prototype/LICENSE +0 -21
- package/skills/prototype/LOGIC.md +0 -67
- package/skills/prototype/SKILL.md +0 -28
- 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 -14
- package/skills/research/agents/openai.yaml +0 -3
- package/skills/resolving-merge-conflicts/LICENSE +0 -21
- package/skills/resolving-merge-conflicts/SKILL.md +0 -16
- package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
- package/skills/tdd/LICENSE +0 -21
- package/skills/tdd/SKILL.md +0 -40
- 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 -141
- package/skills/teach/agents/openai.yaml +0 -4
- package/skills/to-questionnaire/LICENSE +0 -21
- package/skills/to-questionnaire/SKILL.md +0 -55
- package/skills/to-questionnaire/agents/openai.yaml +0 -4
- package/skills/to-spec/LICENSE +0 -21
- package/skills/to-spec/SKILL.md +0 -76
- package/skills/to-spec/agents/openai.yaml +0 -4
- package/skills/to-tickets/LICENSE +0 -21
- package/skills/to-tickets/SKILL.md +0 -106
- package/skills/to-tickets/agents/openai.yaml +0 -4
- package/skills/wait-what/LICENSE +0 -21
- package/skills/wait-what/SKILL.md +0 -8
- package/skills/wait-what/agents/openai.yaml +0 -4
- package/skills/wayfinder/LICENSE +0 -21
- package/skills/wayfinder/SKILL.md +0 -129
- package/skills/wayfinder/agents/openai.yaml +0 -4
- 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 -83
- package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/conversations.js
CHANGED
|
@@ -3,39 +3,116 @@ 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 { finalRefusal, HouseRefusal } from './api.js';
|
|
7
8
|
import { openBridge } from './bridge.js';
|
|
8
9
|
import { CLIS } from './clis.js';
|
|
9
|
-
import { readerFor } from './events.js';
|
|
10
|
-
import {
|
|
10
|
+
import { answeredFailure, readerFor } from './events.js';
|
|
11
|
+
import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
|
|
11
12
|
import { agentBase } from './home.js';
|
|
12
13
|
import { instructions } from './instructions.js';
|
|
13
14
|
import { Parts } from './parts.js';
|
|
15
|
+
import { refusalOf } from './refusals.js';
|
|
16
|
+
import { relayed } from './relay.js';
|
|
14
17
|
import { killMarked, Scope } from './scope.js';
|
|
15
18
|
import { holdSecretInput } from './secret-input.js';
|
|
16
|
-
import { createHowWeWork } from './skills.js';
|
|
17
19
|
import { transcribe } from './transcription.js';
|
|
18
20
|
import { workFor } from './work.js';
|
|
19
21
|
const KILLED = 'the conversation was killed';
|
|
22
|
+
const INTERRUPTED = 'the conversation was interrupted';
|
|
23
|
+
const REFUSED = 'the CLI refused the prompt';
|
|
24
|
+
const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
|
|
25
|
+
const GONE = 'House no longer holds this conversation; start a new one';
|
|
26
|
+
const RETIRED = "this conversation's Agent is retired, so it cannot start";
|
|
27
|
+
const ENDED = {
|
|
28
|
+
conversation_not_found: GONE,
|
|
29
|
+
conversation_historical: RETIRED,
|
|
30
|
+
};
|
|
31
|
+
const NO_START = "the Agent's CLI did not start";
|
|
32
|
+
const NO_SESSION = "the Agent's CLI did not open a session";
|
|
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";
|
|
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";
|
|
37
|
+
class NotStarted extends Error {
|
|
38
|
+
}
|
|
39
|
+
class ResumeRefused extends NotStarted {
|
|
40
|
+
}
|
|
41
|
+
async function step(reason, work) {
|
|
42
|
+
try {
|
|
43
|
+
return await work;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
|
|
47
|
+
}
|
|
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;
|
|
54
|
+
function unbridged(error) {
|
|
55
|
+
return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
|
|
56
|
+
}
|
|
57
|
+
const EXIT_PLAN_MODE = {
|
|
58
|
+
parse(value) {
|
|
59
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
60
|
+
throw new Error('_x.ai/exit_plan_mode needs an object');
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
|
|
20
66
|
function causeOf(error) {
|
|
21
67
|
return error instanceof Error ? error.message : String(error);
|
|
22
68
|
}
|
|
69
|
+
function answeredWith(error) {
|
|
70
|
+
return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
71
|
+
}
|
|
72
|
+
function exitedWith(running, cause) {
|
|
73
|
+
if (running.killed)
|
|
74
|
+
return { failure: 'killed', cause: KILLED };
|
|
75
|
+
return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
|
|
76
|
+
}
|
|
23
77
|
function answerMessage(request, response) {
|
|
24
78
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
25
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
|
+
}
|
|
26
86
|
function logged(error) {
|
|
27
87
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
28
88
|
}
|
|
89
|
+
function refusal(conversation, error) {
|
|
90
|
+
if (conversation.kills > 0)
|
|
91
|
+
return KILLED;
|
|
92
|
+
const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
|
|
93
|
+
logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
|
|
94
|
+
return causeOf(error);
|
|
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
|
+
}
|
|
29
102
|
function marker(bridge) {
|
|
30
103
|
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
31
104
|
}
|
|
105
|
+
function steers(adapter) {
|
|
106
|
+
return adapter.initialized._meta?.steering?.supported === true;
|
|
107
|
+
}
|
|
32
108
|
class Turn {
|
|
33
109
|
id = randomUUID();
|
|
34
110
|
parts;
|
|
35
|
-
admitted = false;
|
|
36
111
|
questions = 0;
|
|
37
112
|
secrets = 0;
|
|
38
113
|
prompted = false;
|
|
114
|
+
steered = false;
|
|
115
|
+
taken = null;
|
|
39
116
|
ended = false;
|
|
40
117
|
images = 0;
|
|
41
118
|
uploads = [];
|
|
@@ -49,20 +126,31 @@ class Conversation {
|
|
|
49
126
|
session = null;
|
|
50
127
|
turn = null;
|
|
51
128
|
commands = null;
|
|
129
|
+
options = null;
|
|
130
|
+
exitUntold = false;
|
|
52
131
|
context = null;
|
|
53
132
|
leftovers = new Set();
|
|
54
133
|
waiting = [];
|
|
134
|
+
done = false;
|
|
55
135
|
sending = null;
|
|
56
136
|
subagents = new Map();
|
|
57
137
|
held = [];
|
|
58
138
|
opening = null;
|
|
139
|
+
bridge = null;
|
|
140
|
+
launch = null;
|
|
141
|
+
prompted = null;
|
|
59
142
|
kills = 0;
|
|
60
143
|
killed = new AbortController();
|
|
61
144
|
interrupts = 0;
|
|
62
145
|
interrupted = new AbortController();
|
|
63
|
-
|
|
146
|
+
turns = new Set();
|
|
64
147
|
queue = Promise.resolve();
|
|
65
148
|
reports = Promise.resolve();
|
|
149
|
+
starting = null;
|
|
150
|
+
preparing = null;
|
|
151
|
+
told = null;
|
|
152
|
+
untold = false;
|
|
153
|
+
shown = null;
|
|
66
154
|
constructor(id) {
|
|
67
155
|
this.id = id;
|
|
68
156
|
}
|
|
@@ -78,9 +166,7 @@ function asksSecret(request) {
|
|
|
78
166
|
export class Conversations {
|
|
79
167
|
kit;
|
|
80
168
|
conversations = new Map();
|
|
81
|
-
acks = new Map();
|
|
82
169
|
questions = new Map();
|
|
83
|
-
carrying = 0;
|
|
84
170
|
reporting = 0;
|
|
85
171
|
watched = false;
|
|
86
172
|
constructor(kit) {
|
|
@@ -88,43 +174,32 @@ export class Conversations {
|
|
|
88
174
|
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
89
175
|
setInterval(() => this.look(), 2_000);
|
|
90
176
|
}
|
|
91
|
-
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
conversation.queue = handled.catch(() => undefined);
|
|
109
|
-
const work = handled.then(({ ack }) => ack);
|
|
110
|
-
this.acks.set(input.input_id, work);
|
|
111
|
-
return work;
|
|
112
|
-
})();
|
|
113
|
-
carried
|
|
114
|
-
.then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
|
|
115
|
-
.catch(logged)
|
|
116
|
-
.finally(() => {
|
|
117
|
-
this.carrying--;
|
|
118
|
-
this.kit.changed();
|
|
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') {
|
|
181
|
+
conversation.kills++;
|
|
182
|
+
conversation.killed.abort(new Error(KILLED));
|
|
183
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
184
|
+
waiting.settle({ refused: KILLED });
|
|
185
|
+
this.halt(conversation);
|
|
186
|
+
}
|
|
187
|
+
if (call.kind === 'interrupt') {
|
|
188
|
+
conversation.interrupts++;
|
|
189
|
+
conversation.interrupted.abort();
|
|
190
|
+
}
|
|
191
|
+
const handled = conversation.queue.then(() => this.carry(conversation, call));
|
|
192
|
+
conversation.queue = handled.catch(() => undefined);
|
|
193
|
+
return handled.then(async ({ result }) => ({ result: await result }));
|
|
119
194
|
});
|
|
120
195
|
}
|
|
121
196
|
idle() {
|
|
122
|
-
if (this.
|
|
197
|
+
if (this.reporting > 0)
|
|
123
198
|
return false;
|
|
124
199
|
for (const conversation of this.conversations.values()) {
|
|
125
200
|
const turn = conversation.turn;
|
|
126
201
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
127
|
-
if (conversation.
|
|
202
|
+
if (conversation.turns.size > (asking ? 1 : 0))
|
|
128
203
|
return false;
|
|
129
204
|
}
|
|
130
205
|
const conversations = [...this.conversations.values()];
|
|
@@ -132,6 +207,9 @@ export class Conversations {
|
|
|
132
207
|
return false;
|
|
133
208
|
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
134
209
|
}
|
|
210
|
+
quiet() {
|
|
211
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
|
|
212
|
+
}
|
|
135
213
|
look() {
|
|
136
214
|
for (const conversation of this.conversations.values()) {
|
|
137
215
|
for (const scope of conversation.leftovers) {
|
|
@@ -145,6 +223,16 @@ export class Conversations {
|
|
|
145
223
|
}
|
|
146
224
|
opened() {
|
|
147
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
|
+
}
|
|
148
236
|
}
|
|
149
237
|
watching(watched) {
|
|
150
238
|
const resend = watched && !this.watched;
|
|
@@ -164,130 +252,320 @@ export class Conversations {
|
|
|
164
252
|
}
|
|
165
253
|
return conversation;
|
|
166
254
|
}
|
|
167
|
-
async carry(conversation,
|
|
168
|
-
if (
|
|
169
|
-
return
|
|
170
|
-
if (
|
|
171
|
-
return this.message(conversation,
|
|
172
|
-
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') {
|
|
173
267
|
if (--conversation.interrupts === 0)
|
|
174
268
|
conversation.interrupted = new AbortController();
|
|
175
|
-
await this.interrupt(conversation, String(
|
|
269
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
176
270
|
}
|
|
177
|
-
if (
|
|
271
|
+
if (call.kind === 'kill') {
|
|
178
272
|
await this.kill(conversation);
|
|
179
273
|
await conversation.sending;
|
|
180
274
|
if (--conversation.kills === 0)
|
|
181
275
|
conversation.killed = new AbortController();
|
|
182
276
|
}
|
|
183
|
-
if (
|
|
184
|
-
await this.option(conversation, String(
|
|
185
|
-
if (
|
|
186
|
-
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));
|
|
187
281
|
if (question === undefined) {
|
|
188
|
-
const text = answerMessage(
|
|
189
|
-
return this.message(conversation, { ...
|
|
282
|
+
const text = answerMessage(call.request, call.response);
|
|
283
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
190
284
|
}
|
|
191
|
-
question.answer(
|
|
285
|
+
question.answer(call.response);
|
|
192
286
|
}
|
|
193
|
-
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
|
+
};
|
|
194
295
|
}
|
|
195
|
-
async
|
|
196
|
-
|
|
197
|
-
|
|
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;
|
|
198
344
|
try {
|
|
199
|
-
|
|
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);
|
|
200
352
|
}
|
|
201
353
|
catch (error) {
|
|
202
|
-
|
|
354
|
+
this.closeIdleBridge(conversation);
|
|
355
|
+
this.prepares(conversation, null);
|
|
356
|
+
return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
203
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 };
|
|
204
367
|
}
|
|
205
|
-
async
|
|
206
|
-
|
|
207
|
-
|
|
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;
|
|
208
388
|
try {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
content.push({ type: 'text', text: lines.join('\n') });
|
|
215
|
-
if (conversation.kills > 0)
|
|
216
|
-
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
|
+
}));
|
|
217
394
|
}
|
|
218
395
|
catch (error) {
|
|
219
|
-
|
|
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 })) });
|
|
220
412
|
}
|
|
221
|
-
return {
|
|
222
|
-
ack: new Promise((settle) => {
|
|
223
|
-
conversation.waiting.push({ input, route, content, settle });
|
|
224
|
-
this.next(conversation);
|
|
225
|
-
}),
|
|
226
|
-
};
|
|
227
413
|
}
|
|
228
414
|
next(conversation) {
|
|
229
|
-
if (conversation.sending !== null || conversation.
|
|
415
|
+
if (conversation.sending !== null || conversation.turns.size > 0)
|
|
230
416
|
return;
|
|
231
417
|
const waiting = conversation.waiting.shift();
|
|
232
|
-
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);
|
|
233
424
|
return;
|
|
425
|
+
}
|
|
234
426
|
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
235
427
|
conversation.sending = null;
|
|
236
428
|
this.next(conversation);
|
|
237
429
|
});
|
|
238
430
|
}
|
|
239
431
|
async send(conversation, waiting) {
|
|
240
|
-
const {
|
|
241
|
-
let
|
|
432
|
+
const { call } = waiting;
|
|
433
|
+
let result = {};
|
|
434
|
+
let started = false;
|
|
435
|
+
let instructing = waiting.instructing;
|
|
242
436
|
const prompt = [];
|
|
243
437
|
try {
|
|
244
438
|
if (conversation.running === null) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (resumed === null)
|
|
248
|
-
ack = { provider_session_id: opened };
|
|
439
|
+
this.starts(conversation, 'session');
|
|
440
|
+
await this.launched(conversation);
|
|
249
441
|
}
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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;
|
|
449
|
+
}
|
|
450
|
+
const session = conversation.running.sessionId;
|
|
451
|
+
if (started || (call.first === true && session !== call.provider_session_id)) {
|
|
452
|
+
result = { provider_session_id: session };
|
|
453
|
+
}
|
|
454
|
+
if (call.first === true) {
|
|
455
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
255
456
|
}
|
|
256
457
|
if (conversation.kills > 0)
|
|
257
458
|
throw new Error(KILLED);
|
|
258
459
|
}
|
|
259
460
|
catch (error) {
|
|
260
|
-
|
|
461
|
+
this.starts(conversation, null);
|
|
462
|
+
if (started) {
|
|
261
463
|
conversation.session = null;
|
|
262
464
|
await this.kill(conversation);
|
|
263
465
|
}
|
|
264
|
-
|
|
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 });
|
|
265
469
|
return;
|
|
266
470
|
}
|
|
267
|
-
this.
|
|
268
|
-
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);
|
|
269
474
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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}` });
|
|
285
531
|
}
|
|
286
532
|
}
|
|
287
|
-
|
|
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);
|
|
288
562
|
}
|
|
289
563
|
async interrupt(conversation, turnId) {
|
|
290
|
-
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)
|
|
291
569
|
return;
|
|
292
570
|
await conversation.running.adapter.connection.agent.notify('session/cancel', {
|
|
293
571
|
sessionId: conversation.running.sessionId,
|
|
@@ -339,63 +617,78 @@ export class Conversations {
|
|
|
339
617
|
}
|
|
340
618
|
return route;
|
|
341
619
|
}
|
|
342
|
-
async start(conversation,
|
|
620
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
621
|
+
const route = await routed;
|
|
343
622
|
const reader = readerFor(route.kind);
|
|
344
623
|
reader.session = sessionId;
|
|
345
624
|
const app = client({ name: '@agentshouse/kit' })
|
|
346
625
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
347
|
-
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
626
|
+
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
627
|
+
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
|
|
348
628
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
349
|
-
const
|
|
629
|
+
const bridging = this.bridged(conversation);
|
|
630
|
+
const bridge = await step(unbridged, bridging);
|
|
350
631
|
if (conversation.kills > 0) {
|
|
351
|
-
|
|
632
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
352
633
|
throw new Error(KILLED);
|
|
353
634
|
}
|
|
354
635
|
conversation.opening = bridge;
|
|
355
636
|
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
356
637
|
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
638
|
+
let adapter = null;
|
|
357
639
|
try {
|
|
358
|
-
|
|
640
|
+
bridged(bridge);
|
|
359
641
|
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
360
|
-
const
|
|
642
|
+
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
643
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
361
644
|
work.read(message);
|
|
362
645
|
this.read(conversation, reader, message);
|
|
363
|
-
}, { ...bridge.env, ...
|
|
364
|
-
void adapter.exited.then(() =>
|
|
646
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
647
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
365
648
|
const agent = adapter.connection.agent;
|
|
366
649
|
const cwd = route.working_directory;
|
|
367
650
|
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
|
|
651
|
+
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
652
|
+
const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
371
653
|
reader.session = opened.sessionId;
|
|
372
|
-
if (sessionId
|
|
654
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
373
655
|
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
374
|
-
await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
|
|
656
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
375
657
|
}
|
|
376
|
-
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
377
|
-
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
658
|
+
const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
659
|
+
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
378
660
|
void work.settled().then(() => scope.record());
|
|
379
661
|
conversation.running = running;
|
|
662
|
+
adapter.turn(conversation.turns.size > 0);
|
|
380
663
|
conversation.session = running.sessionId;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
this.options(conversation, options);
|
|
385
|
-
void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
|
|
664
|
+
conversation.options = options;
|
|
665
|
+
this.facts(conversation);
|
|
666
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
386
667
|
return running.sessionId;
|
|
387
668
|
}
|
|
388
669
|
catch (error) {
|
|
389
|
-
|
|
670
|
+
adapter?.retire();
|
|
390
671
|
work.close();
|
|
391
672
|
conversation.leftovers.add(scope);
|
|
392
|
-
|
|
673
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
393
674
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
394
675
|
}
|
|
395
676
|
finally {
|
|
396
677
|
conversation.opening = null;
|
|
397
678
|
}
|
|
398
679
|
}
|
|
680
|
+
async resume(conversation, kind, adapter, request, empty) {
|
|
681
|
+
try {
|
|
682
|
+
return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
|
|
686
|
+
throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
|
|
687
|
+
}
|
|
688
|
+
logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
399
692
|
async launchSettings(running, route, offered) {
|
|
400
693
|
let options = offered;
|
|
401
694
|
const settings = [
|
|
@@ -417,27 +710,45 @@ export class Conversations {
|
|
|
417
710
|
}
|
|
418
711
|
return options;
|
|
419
712
|
}
|
|
420
|
-
exited(conversation,
|
|
421
|
-
const running = conversation.running;
|
|
422
|
-
if (running?.adapter !== adapter)
|
|
423
|
-
return;
|
|
713
|
+
exited(conversation, running, cause) {
|
|
424
714
|
this.detach(conversation, running);
|
|
425
715
|
if (conversation.turn !== null) {
|
|
426
|
-
this.end(conversation, conversation.turn,
|
|
716
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
427
717
|
}
|
|
428
718
|
this.next(conversation);
|
|
429
719
|
this.kit.changed();
|
|
430
720
|
}
|
|
431
721
|
detach(conversation, running) {
|
|
722
|
+
if (conversation.bridge === running.bridging)
|
|
723
|
+
conversation.bridge = null;
|
|
432
724
|
running.work.close();
|
|
433
725
|
conversation.leftovers.add(running.scope);
|
|
434
726
|
conversation.running = null;
|
|
435
727
|
conversation.commands = null;
|
|
728
|
+
conversation.options = null;
|
|
729
|
+
if (conversation.subagents.size > 0) {
|
|
730
|
+
conversation.subagents.clear();
|
|
731
|
+
this.subagentsChanged(conversation);
|
|
732
|
+
}
|
|
436
733
|
for (const [id, question] of this.questions) {
|
|
437
734
|
if (question.conversation === conversation)
|
|
438
735
|
this.questions.delete(id);
|
|
439
736
|
}
|
|
440
|
-
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 });
|
|
441
752
|
}
|
|
442
753
|
commands(conversation, commands) {
|
|
443
754
|
conversation.commands = commands;
|
|
@@ -448,6 +759,7 @@ export class Conversations {
|
|
|
448
759
|
options(conversation, options) {
|
|
449
760
|
if (conversation.running === null)
|
|
450
761
|
return;
|
|
762
|
+
conversation.options = options;
|
|
451
763
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
452
764
|
}
|
|
453
765
|
deliver(conversation, turn, event, body) {
|
|
@@ -462,7 +774,7 @@ export class Conversations {
|
|
|
462
774
|
return delivered;
|
|
463
775
|
}
|
|
464
776
|
write(conversation, turn, operation) {
|
|
465
|
-
if (
|
|
777
|
+
if (turn.ended || !this.watched)
|
|
466
778
|
return;
|
|
467
779
|
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
468
780
|
}
|
|
@@ -472,13 +784,9 @@ export class Conversations {
|
|
|
472
784
|
}
|
|
473
785
|
begin(conversation) {
|
|
474
786
|
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
475
|
-
conversation.
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
turn.admitted = true;
|
|
479
|
-
this.replay(conversation, turn);
|
|
480
|
-
})
|
|
481
|
-
.catch(logged);
|
|
787
|
+
conversation.turns.add(turn);
|
|
788
|
+
conversation.running?.adapter.turn(true);
|
|
789
|
+
void this.deliver(conversation, turn, 'started', {}).catch(logged);
|
|
482
790
|
conversation.turn = turn;
|
|
483
791
|
if (conversation.context !== null)
|
|
484
792
|
turn.parts.measure(conversation.context);
|
|
@@ -487,59 +795,81 @@ export class Conversations {
|
|
|
487
795
|
this.kit.changed();
|
|
488
796
|
return turn;
|
|
489
797
|
}
|
|
490
|
-
end(conversation, turn, failed) {
|
|
798
|
+
end(conversation, turn, failed, notice = null) {
|
|
491
799
|
if (turn.ended)
|
|
492
800
|
return;
|
|
801
|
+
if (failed !== null)
|
|
802
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
493
803
|
if (conversation.turn === turn) {
|
|
494
804
|
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
495
805
|
this.apply(conversation, turn, event);
|
|
496
806
|
}
|
|
807
|
+
if (notice !== null)
|
|
808
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
497
809
|
turn.ended = true;
|
|
498
|
-
conversation.
|
|
810
|
+
conversation.turns.delete(turn);
|
|
811
|
+
conversation.running?.adapter.turn(conversation.turns.size > 0);
|
|
499
812
|
if (conversation.turn === turn)
|
|
500
813
|
conversation.turn = null;
|
|
501
814
|
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
502
|
-
parts: turn.parts.
|
|
815
|
+
parts: turn.parts.saved(),
|
|
503
816
|
context: turn.parts.context,
|
|
504
|
-
...failed,
|
|
817
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
505
818
|
}));
|
|
506
819
|
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
507
820
|
this.next(conversation);
|
|
508
821
|
this.kit.changed();
|
|
509
822
|
}
|
|
510
|
-
prompt(conversation, prompt) {
|
|
823
|
+
prompt(conversation, prompt, taken) {
|
|
511
824
|
const running = conversation.running;
|
|
512
825
|
const turn = this.begin(conversation);
|
|
513
826
|
turn.prompted = true;
|
|
514
|
-
|
|
827
|
+
turn.taken = taken;
|
|
828
|
+
conversation.prompted = running.sessionId;
|
|
829
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
830
|
+
this.took(turn);
|
|
831
|
+
const notice = answeredFailure(answer);
|
|
832
|
+
if (notice !== null) {
|
|
833
|
+
this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
837
|
+
}
|
|
838
|
+
}, async (error) => {
|
|
515
839
|
if (running.adapter.connection.signal.aborted) {
|
|
516
|
-
this.end(conversation, turn,
|
|
840
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
517
841
|
}
|
|
518
842
|
else {
|
|
519
|
-
|
|
520
|
-
|
|
843
|
+
this.took(turn);
|
|
844
|
+
if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
|
|
845
|
+
running.adapter.retire();
|
|
521
846
|
this.detach(conversation, running);
|
|
522
847
|
}
|
|
523
|
-
this.end(conversation, turn,
|
|
848
|
+
this.end(conversation, turn, answeredWith(error));
|
|
524
849
|
}
|
|
525
850
|
});
|
|
526
851
|
}
|
|
527
852
|
started(conversation) {
|
|
528
|
-
|
|
853
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
854
|
+
this.took(turn);
|
|
855
|
+
return turn;
|
|
529
856
|
}
|
|
530
|
-
ended(conversation) {
|
|
857
|
+
ended(conversation, stop) {
|
|
531
858
|
const turn = conversation.turn;
|
|
532
859
|
if (turn === null)
|
|
533
860
|
return;
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
this.apply(conversation, turn, event);
|
|
540
|
-
conversation.turn = null;
|
|
541
|
-
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;
|
|
542
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();
|
|
543
873
|
}
|
|
544
874
|
async ask(conversation, method, params, secret, signal) {
|
|
545
875
|
const turn = this.started(conversation);
|
|
@@ -595,7 +925,7 @@ export class Conversations {
|
|
|
595
925
|
this.started(conversation);
|
|
596
926
|
return;
|
|
597
927
|
case 'ended':
|
|
598
|
-
this.ended(conversation);
|
|
928
|
+
this.ended(conversation, event.stop);
|
|
599
929
|
return;
|
|
600
930
|
case 'commands':
|
|
601
931
|
this.commands(conversation, event.commands);
|
|
@@ -610,6 +940,9 @@ export class Conversations {
|
|
|
610
940
|
case 'subagent-done':
|
|
611
941
|
this.subagentDone(conversation, event.id, event.result);
|
|
612
942
|
return;
|
|
943
|
+
case 'subagent-activity':
|
|
944
|
+
this.subagentActivity(conversation, event.id, event.activity);
|
|
945
|
+
return;
|
|
613
946
|
case 'marker':
|
|
614
947
|
case 'image':
|
|
615
948
|
if (conversation.turn !== null)
|
|
@@ -620,6 +953,11 @@ export class Conversations {
|
|
|
620
953
|
}
|
|
621
954
|
this.apply(conversation, this.started(conversation), event);
|
|
622
955
|
}
|
|
956
|
+
took(turn) {
|
|
957
|
+
const taken = turn.taken;
|
|
958
|
+
turn.taken = null;
|
|
959
|
+
taken?.();
|
|
960
|
+
}
|
|
623
961
|
apply(conversation, turn, event) {
|
|
624
962
|
switch (event.kind) {
|
|
625
963
|
case 'text':
|
|
@@ -631,7 +969,8 @@ export class Conversations {
|
|
|
631
969
|
return;
|
|
632
970
|
case 'subagent': {
|
|
633
971
|
const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
|
|
634
|
-
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);
|
|
635
974
|
return;
|
|
636
975
|
}
|
|
637
976
|
case 'message':
|
|
@@ -644,7 +983,7 @@ export class Conversations {
|
|
|
644
983
|
turn.parts.plan(event.entries);
|
|
645
984
|
return;
|
|
646
985
|
case 'image':
|
|
647
|
-
turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
|
|
986
|
+
turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
|
|
648
987
|
this.kit.changed();
|
|
649
988
|
return;
|
|
650
989
|
}
|
|
@@ -654,6 +993,7 @@ export class Conversations {
|
|
|
654
993
|
if (subagent === undefined)
|
|
655
994
|
return;
|
|
656
995
|
conversation.subagents.delete(id);
|
|
996
|
+
this.subagentsChanged(conversation);
|
|
657
997
|
if (subagent.turn === conversation.turn) {
|
|
658
998
|
subagent.turn.parts.done(subagent.index, result);
|
|
659
999
|
return;
|
|
@@ -664,23 +1004,37 @@ export class Conversations {
|
|
|
664
1004
|
else
|
|
665
1005
|
conversation.turn.parts.start(part);
|
|
666
1006
|
}
|
|
667
|
-
|
|
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
|
+
}
|
|
1014
|
+
async image(conversation, turn, reserved, data, mediaType) {
|
|
668
1015
|
const bytes = Buffer.from(data, 'base64');
|
|
669
1016
|
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
1017
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
670
1018
|
try {
|
|
671
1019
|
const declared = (await this.deliver(conversation, turn, 'images', {
|
|
672
1020
|
name,
|
|
673
1021
|
media_type: mediaType,
|
|
674
1022
|
bytes: bytes.length,
|
|
675
|
-
sha256
|
|
1023
|
+
sha256,
|
|
676
1024
|
}));
|
|
677
|
-
const saved =
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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);
|
|
681
1034
|
}
|
|
682
1035
|
catch (error) {
|
|
683
|
-
|
|
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);
|
|
684
1038
|
}
|
|
685
1039
|
}
|
|
686
1040
|
}
|