@agentshouse/kit 0.2.1-ce145e9aadbf6 → 0.2.1-ce3d2bb67177c
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 +40 -39
- package/dist/acp.js +48 -105
- package/dist/agents.js +153 -43
- package/dist/api.js +34 -2
- package/dist/app-git.js +48 -0
- package/dist/bootstrap.json +4 -4
- package/dist/bridge.js +36 -57
- package/dist/calls.js +63 -0
- package/dist/chatgpt.js +32 -0
- package/dist/clis.js +127 -70
- package/dist/codex-main.js +58 -0
- package/dist/connection.js +125 -0
- package/dist/conversations.js +620 -323
- package/dist/copies.js +188 -128
- package/dist/end.js +27 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +525 -0
- package/dist/files.js +123 -17
- package/dist/git.js +11 -9
- package/dist/home.js +96 -7
- package/dist/house-cli.js +327 -0
- package/dist/house-main.js +7 -159
- package/dist/instructions.js +10 -32
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -19
- package/dist/login.js +117 -104
- package/dist/logout.js +14 -13
- package/dist/mcp.js +4 -2
- package/dist/parts.js +82 -0
- package/dist/placement.js +39 -0
- package/dist/refusals.js +12 -0
- package/dist/relay.js +12 -2
- package/dist/resident.js +106 -50
- package/dist/scope.js +206 -0
- package/dist/secret-input.js +3 -2
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +37 -31
- package/dist/skills.js +84 -15
- package/dist/transcription.js +100 -21
- package/dist/translate.js +2 -8
- package/dist/update.js +71 -50
- package/dist/wire.js +22 -0
- package/dist/work.js +346 -0
- package/how-we-work.md +13 -3
- package/package.json +4 -3
- package/skills/ask-matt/SKILL.md +17 -11
- package/skills/ask-matt/agents/openai.yaml +1 -0
- package/skills/code-review/SKILL.md +5 -5
- package/skills/codebase-design/DESIGN-IT-TWICE.md +1 -1
- package/skills/codebase-design/SKILL.md +0 -2
- package/skills/diagnosing-bugs/SKILL.md +2 -4
- package/skills/domain-modeling/{CONTEXT-FORMAT.md → GLOSSARY-FORMAT.md} +10 -10
- package/skills/domain-modeling/SKILL.md +12 -14
- package/skills/grill-me/SKILL.md +1 -2
- package/skills/grill-me/agents/openai.yaml +1 -0
- package/skills/grill-with-docs/SKILL.md +1 -2
- package/skills/grill-with-docs/agents/openai.yaml +1 -0
- package/skills/grilling/SKILL.md +2 -2
- package/skills/handoff/SKILL.md +2 -3
- package/skills/handoff/agents/openai.yaml +1 -0
- package/skills/implement/SKILL.md +5 -4
- package/skills/implement/agents/openai.yaml +1 -0
- package/skills/implement-spec/SKILL.md +40 -0
- package/skills/implement-spec/agents/openai.yaml +5 -0
- package/skills/improve-codebase-architecture/SKILL.md +6 -7
- package/skills/improve-codebase-architecture/agents/openai.yaml +1 -0
- package/skills/pr/CREDITS.md +3 -0
- package/skills/pr/LICENSE +21 -0
- package/skills/pr/SKILL.md +170 -0
- package/skills/pr/agents/openai.yaml +3 -0
- package/skills/prototype/SKILL.md +0 -2
- package/skills/research/SKILL.md +0 -2
- package/skills/retro/LICENSE +21 -0
- package/skills/retro/SKILL.md +44 -0
- package/skills/retro/agents/openai.yaml +5 -0
- package/skills/tdd/SKILL.md +2 -4
- package/skills/teach/SKILL.md +3 -4
- package/skills/teach/agents/openai.yaml +1 -0
- package/skills/to-questionnaire/SKILL.md +1 -2
- package/skills/to-questionnaire/agents/openai.yaml +1 -0
- package/skills/to-spec/SKILL.md +1 -2
- package/skills/to-spec/agents/openai.yaml +1 -0
- package/skills/to-tickets/SKILL.md +3 -4
- package/skills/to-tickets/agents/openai.yaml +1 -0
- package/skills/triage/AGENT-BRIEF.md +207 -0
- package/skills/triage/LICENSE +21 -0
- package/skills/triage/OUT-OF-SCOPE.md +105 -0
- package/skills/triage/SKILL.md +112 -0
- package/skills/triage/agents/openai.yaml +5 -0
- package/skills/wait-what/SKILL.md +2 -3
- package/skills/wait-what/agents/openai.yaml +1 -0
- package/skills/wayfinder/SKILL.md +5 -6
- package/skills/wayfinder/agents/openai.yaml +1 -0
- package/skills/wizard/LICENSE +21 -0
- package/skills/wizard/SKILL.md +44 -0
- package/skills/wizard/agents/openai.yaml +3 -0
- package/skills/wizard/template.sh +223 -0
- package/skills/writing-for-agents/SKILL.md +0 -2
- package/dist/blocks.js +0 -51
- package/dist/stream.js +0 -47
- package/skills/resolving-merge-conflicts/SKILL.md +0 -16
- package/skills/resolving-merge-conflicts/agents/openai.yaml +0 -3
- /package/skills/{resolving-merge-conflicts → implement-spec}/LICENSE +0 -0
package/dist/conversations.js
CHANGED
|
@@ -1,56 +1,141 @@
|
|
|
1
|
-
import { client, } from '@agentclientprotocol/sdk';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { client, RequestError, } from '@agentclientprotocol/sdk';
|
|
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 {
|
|
7
|
-
import {
|
|
6
|
+
import { 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
|
-
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';
|
|
14
|
+
import { Parts } from './parts.js';
|
|
15
|
+
import { refusalOf } from './refusals.js';
|
|
16
|
+
import { relayed } from './relay.js';
|
|
17
|
+
import { killMarked, Scope } from './scope.js';
|
|
13
18
|
import { holdSecretInput } from './secret-input.js';
|
|
14
19
|
import { createHowWeWork } from './skills.js';
|
|
15
20
|
import { transcribe } from './transcription.js';
|
|
21
|
+
import { workFor } from './work.js';
|
|
22
|
+
const SIGNED_OUT = RequestError.authRequired().code;
|
|
16
23
|
const KILLED = 'the conversation was killed';
|
|
24
|
+
const INTERRUPTED = 'the conversation was interrupted';
|
|
25
|
+
const REFUSED = 'the CLI refused the prompt';
|
|
26
|
+
const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
|
|
27
|
+
const GONE = 'House no longer holds this conversation; start a new one';
|
|
28
|
+
const RETIRED = "this conversation's Agent is retired, so it cannot start";
|
|
29
|
+
const ENDED = {
|
|
30
|
+
conversation_not_found: GONE,
|
|
31
|
+
conversation_historical: RETIRED,
|
|
32
|
+
};
|
|
33
|
+
const NO_START = "the Agent's CLI did not start";
|
|
34
|
+
const NO_SESSION = "the Agent's CLI did not open a session";
|
|
35
|
+
const NO_RESUME = "the Agent's CLI could not resume this conversation";
|
|
36
|
+
const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
|
|
37
|
+
const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
|
|
38
|
+
class NotStarted extends Error {
|
|
39
|
+
}
|
|
40
|
+
class ResumeRefused extends NotStarted {
|
|
41
|
+
}
|
|
42
|
+
async function step(reason, work) {
|
|
43
|
+
try {
|
|
44
|
+
return await work;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function signedOut(error) {
|
|
51
|
+
return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
|
|
52
|
+
}
|
|
53
|
+
function unopened(reason) {
|
|
54
|
+
return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
|
|
55
|
+
}
|
|
56
|
+
// A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
|
|
57
|
+
const STARTING_SHOWN_AFTER_MS = 1_000;
|
|
58
|
+
function unbridged(error) {
|
|
59
|
+
return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
|
|
60
|
+
}
|
|
61
|
+
const EXIT_PLAN_MODE = {
|
|
62
|
+
parse(value) {
|
|
63
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
64
|
+
throw new Error('_x.ai/exit_plan_mode needs an object');
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
|
|
17
70
|
function causeOf(error) {
|
|
18
71
|
return error instanceof Error ? error.message : String(error);
|
|
19
72
|
}
|
|
73
|
+
function answeredWith(error) {
|
|
74
|
+
return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
75
|
+
}
|
|
76
|
+
function exitedWith(running, cause) {
|
|
77
|
+
return running.killed ? { failure: 'killed', cause: KILLED } : { failure: 'exited', cause };
|
|
78
|
+
}
|
|
20
79
|
function answerMessage(request, response) {
|
|
21
80
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
22
81
|
}
|
|
82
|
+
async function sentPart(url, bytes) {
|
|
83
|
+
const answer = await fetch(relayed(url), { method: 'PUT', body: bytes });
|
|
84
|
+
await answer.arrayBuffer();
|
|
85
|
+
if (!answer.ok)
|
|
86
|
+
throw storeRefusal('upload', answer.status);
|
|
87
|
+
}
|
|
23
88
|
function logged(error) {
|
|
24
89
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
25
90
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
91
|
+
function refusal(conversation, error) {
|
|
92
|
+
if (conversation.kills > 0)
|
|
93
|
+
return KILLED;
|
|
94
|
+
const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
|
|
95
|
+
logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
|
|
96
|
+
return causeOf(error);
|
|
97
|
+
}
|
|
98
|
+
function audioOf(result) {
|
|
99
|
+
if (result.isError === true)
|
|
100
|
+
return null;
|
|
101
|
+
const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
|
|
102
|
+
return file?.media_type?.startsWith('audio/') === true ? file : null;
|
|
103
|
+
}
|
|
104
|
+
function marker(bridge) {
|
|
105
|
+
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
28
106
|
}
|
|
29
107
|
class Turn {
|
|
30
108
|
id = randomUUID();
|
|
31
|
-
|
|
32
|
-
segment = null;
|
|
33
|
-
admitted = false;
|
|
34
|
-
notes = [];
|
|
35
|
-
sent = [];
|
|
36
|
-
unsentFrom = null;
|
|
37
|
-
draftSequence = 0;
|
|
38
|
-
planSequence = 0;
|
|
39
|
-
entries = null;
|
|
109
|
+
parts;
|
|
40
110
|
questions = 0;
|
|
41
111
|
secrets = 0;
|
|
42
|
-
|
|
112
|
+
prompted = false;
|
|
113
|
+
taken = null;
|
|
43
114
|
ended = false;
|
|
115
|
+
images = 0;
|
|
116
|
+
uploads = [];
|
|
117
|
+
constructor(emit) {
|
|
118
|
+
this.parts = new Parts(emit);
|
|
119
|
+
}
|
|
44
120
|
}
|
|
45
121
|
class Conversation {
|
|
46
122
|
id;
|
|
47
123
|
running = null;
|
|
124
|
+
session = null;
|
|
48
125
|
turn = null;
|
|
49
126
|
commands = null;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
127
|
+
options = null;
|
|
128
|
+
exitUntold = false;
|
|
129
|
+
context = null;
|
|
130
|
+
leftovers = new Set();
|
|
131
|
+
waiting = [];
|
|
132
|
+
sending = null;
|
|
133
|
+
subagents = new Map();
|
|
134
|
+
held = [];
|
|
53
135
|
opening = null;
|
|
136
|
+
bridge = null;
|
|
137
|
+
launch = null;
|
|
138
|
+
prompted = null;
|
|
54
139
|
kills = 0;
|
|
55
140
|
killed = new AbortController();
|
|
56
141
|
interrupts = 0;
|
|
@@ -58,6 +143,11 @@ class Conversation {
|
|
|
58
143
|
open = 0;
|
|
59
144
|
queue = Promise.resolve();
|
|
60
145
|
reports = Promise.resolve();
|
|
146
|
+
starting = null;
|
|
147
|
+
preparing = null;
|
|
148
|
+
told = null;
|
|
149
|
+
untold = false;
|
|
150
|
+
shown = null;
|
|
61
151
|
constructor(id) {
|
|
62
152
|
this.id = id;
|
|
63
153
|
}
|
|
@@ -73,55 +163,80 @@ function asksSecret(request) {
|
|
|
73
163
|
export class Conversations {
|
|
74
164
|
kit;
|
|
75
165
|
conversations = new Map();
|
|
76
|
-
acks = new Map();
|
|
77
166
|
questions = new Map();
|
|
78
|
-
carrying = 0;
|
|
79
167
|
reporting = 0;
|
|
168
|
+
watched = false;
|
|
80
169
|
constructor(kit) {
|
|
81
170
|
this.kit = kit;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
})();
|
|
102
|
-
carried
|
|
103
|
-
.then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
|
|
104
|
-
.catch(logged)
|
|
105
|
-
.finally(() => {
|
|
106
|
-
this.carrying--;
|
|
107
|
-
this.kit.changed();
|
|
171
|
+
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
172
|
+
setInterval(() => this.look(), 2_000);
|
|
173
|
+
}
|
|
174
|
+
call(call) {
|
|
175
|
+
void this.kit.calls.carry(call.input_id, () => {
|
|
176
|
+
const conversation = this.conversation(call.conversation_id);
|
|
177
|
+
if (call.kind === 'kill') {
|
|
178
|
+
conversation.kills++;
|
|
179
|
+
conversation.killed.abort(new Error(KILLED));
|
|
180
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
181
|
+
waiting.settle({ refused: KILLED });
|
|
182
|
+
this.halt(conversation);
|
|
183
|
+
}
|
|
184
|
+
if (call.kind === 'interrupt') {
|
|
185
|
+
conversation.interrupts++;
|
|
186
|
+
conversation.interrupted.abort();
|
|
187
|
+
}
|
|
188
|
+
const handled = conversation.queue.then(() => this.carry(conversation, call));
|
|
189
|
+
conversation.queue = handled.catch(() => undefined);
|
|
190
|
+
return handled.then(async ({ result }) => ({ result: await result }));
|
|
108
191
|
});
|
|
109
192
|
}
|
|
110
193
|
idle() {
|
|
111
|
-
if (this.
|
|
194
|
+
if (this.reporting > 0)
|
|
112
195
|
return false;
|
|
113
196
|
for (const conversation of this.conversations.values()) {
|
|
114
197
|
const turn = conversation.turn;
|
|
115
198
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
116
|
-
if (conversation.
|
|
199
|
+
if (conversation.open > (asking ? 1 : 0))
|
|
117
200
|
return false;
|
|
118
201
|
}
|
|
119
|
-
|
|
202
|
+
const conversations = [...this.conversations.values()];
|
|
203
|
+
if (conversations.some((conversation) => conversation.running?.work.busy()))
|
|
204
|
+
return false;
|
|
205
|
+
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
206
|
+
}
|
|
207
|
+
quiet() {
|
|
208
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
|
|
209
|
+
}
|
|
210
|
+
look() {
|
|
211
|
+
for (const conversation of this.conversations.values()) {
|
|
212
|
+
for (const scope of conversation.leftovers) {
|
|
213
|
+
if (!scope.empty())
|
|
214
|
+
continue;
|
|
215
|
+
scope.close();
|
|
216
|
+
conversation.leftovers.delete(scope);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
this.kit.changed();
|
|
120
220
|
}
|
|
121
221
|
opened() {
|
|
222
|
+
this.watched = false;
|
|
223
|
+
for (const conversation of this.conversations.values()) {
|
|
224
|
+
if (conversation.running !== null)
|
|
225
|
+
this.facts(conversation);
|
|
226
|
+
else if (conversation.exitUntold)
|
|
227
|
+
this.process(conversation, false);
|
|
228
|
+
if (conversation.untold || conversation.told !== null)
|
|
229
|
+
this.retell(conversation);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
watching(watched) {
|
|
233
|
+
const resend = watched && !this.watched;
|
|
234
|
+
this.watched = watched;
|
|
235
|
+
if (!resend)
|
|
236
|
+
return;
|
|
122
237
|
for (const conversation of this.conversations.values()) {
|
|
123
238
|
if (conversation.turn !== null)
|
|
124
|
-
conversation.turn
|
|
239
|
+
this.replay(conversation, conversation.turn);
|
|
125
240
|
}
|
|
126
241
|
}
|
|
127
242
|
conversation(id) {
|
|
@@ -132,93 +247,249 @@ export class Conversations {
|
|
|
132
247
|
}
|
|
133
248
|
return conversation;
|
|
134
249
|
}
|
|
135
|
-
async carry(conversation,
|
|
136
|
-
if (
|
|
137
|
-
return this.open(conversation,
|
|
138
|
-
if (
|
|
139
|
-
return this.message(conversation,
|
|
140
|
-
|
|
250
|
+
async carry(conversation, call) {
|
|
251
|
+
if (call.kind === 'open')
|
|
252
|
+
return { result: this.open(conversation, call) };
|
|
253
|
+
if (call.kind === 'message')
|
|
254
|
+
return this.message(conversation, call);
|
|
255
|
+
await this.launched(conversation);
|
|
256
|
+
if (call.kind === 'interrupt') {
|
|
141
257
|
if (--conversation.interrupts === 0)
|
|
142
258
|
conversation.interrupted = new AbortController();
|
|
143
|
-
await this.interrupt(conversation, String(
|
|
259
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
144
260
|
}
|
|
145
|
-
if (
|
|
261
|
+
if (call.kind === 'kill') {
|
|
146
262
|
await this.kill(conversation);
|
|
263
|
+
await conversation.sending;
|
|
147
264
|
if (--conversation.kills === 0)
|
|
148
265
|
conversation.killed = new AbortController();
|
|
149
266
|
}
|
|
150
|
-
if (
|
|
151
|
-
await this.option(conversation, String(
|
|
152
|
-
if (
|
|
153
|
-
const question = this.questions.get(String(
|
|
267
|
+
if (call.kind === 'option')
|
|
268
|
+
await this.option(conversation, String(call.option), call.value);
|
|
269
|
+
if (call.kind === 'answer') {
|
|
270
|
+
const question = this.questions.get(String(call.interaction_id));
|
|
154
271
|
if (question === undefined) {
|
|
155
|
-
|
|
272
|
+
const text = answerMessage(call.request, call.response);
|
|
273
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
156
274
|
}
|
|
157
|
-
question.answer(
|
|
275
|
+
question.answer(call.response);
|
|
158
276
|
}
|
|
159
|
-
return {};
|
|
277
|
+
return { result: Promise.resolve({}) };
|
|
160
278
|
}
|
|
161
|
-
|
|
279
|
+
open(conversation, call) {
|
|
162
280
|
if (conversation.running !== null)
|
|
163
|
-
return {};
|
|
281
|
+
return Promise.resolve({});
|
|
282
|
+
const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
|
|
283
|
+
return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
|
|
284
|
+
}
|
|
285
|
+
launch(conversation, route, sessionId, empty) {
|
|
286
|
+
let opened;
|
|
287
|
+
let failed;
|
|
288
|
+
const bridge = new Promise((resolve, reject) => {
|
|
289
|
+
opened = resolve;
|
|
290
|
+
failed = reject;
|
|
291
|
+
});
|
|
292
|
+
bridge.catch(() => undefined);
|
|
293
|
+
const session = this.start(conversation, route, sessionId, empty, opened);
|
|
294
|
+
session.catch(failed);
|
|
295
|
+
const launch = { bridge, session };
|
|
296
|
+
conversation.launch = launch;
|
|
297
|
+
void session
|
|
298
|
+
.catch(() => undefined)
|
|
299
|
+
.then(() => {
|
|
300
|
+
if (conversation.launch !== launch)
|
|
301
|
+
return;
|
|
302
|
+
conversation.launch = null;
|
|
303
|
+
this.closeIdleBridge(conversation);
|
|
304
|
+
this.next(conversation);
|
|
305
|
+
});
|
|
306
|
+
this.next(conversation);
|
|
307
|
+
return launch;
|
|
308
|
+
}
|
|
309
|
+
async launched(conversation) {
|
|
310
|
+
while (conversation.launch !== null)
|
|
311
|
+
await conversation.launch.session.catch(() => undefined);
|
|
312
|
+
}
|
|
313
|
+
instructed(conversation, route) {
|
|
314
|
+
const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
|
|
315
|
+
if (bridge === undefined)
|
|
316
|
+
return null;
|
|
317
|
+
const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
|
|
318
|
+
read.catch(() => undefined);
|
|
319
|
+
return read;
|
|
320
|
+
}
|
|
321
|
+
async message(conversation, call) {
|
|
322
|
+
const content = [{ type: 'text', text: String(call.text) }];
|
|
323
|
+
let route;
|
|
324
|
+
let instructing = null;
|
|
164
325
|
try {
|
|
165
|
-
|
|
326
|
+
route = await this.route(conversation, call.agent_id);
|
|
327
|
+
if (call.first === true)
|
|
328
|
+
instructing = this.instructed(conversation, route);
|
|
329
|
+
if (call.message_id !== null)
|
|
330
|
+
content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
|
|
331
|
+
if (conversation.kills > 0)
|
|
332
|
+
throw new Error(KILLED);
|
|
166
333
|
}
|
|
167
334
|
catch (error) {
|
|
168
|
-
|
|
335
|
+
this.closeIdleBridge(conversation);
|
|
336
|
+
this.prepares(conversation, null);
|
|
337
|
+
return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
338
|
+
}
|
|
339
|
+
conversation.preparing = null;
|
|
340
|
+
return {
|
|
341
|
+
result: new Promise((settle) => {
|
|
342
|
+
conversation.waiting.push({ call, route, content, instructing, settle });
|
|
343
|
+
this.next(conversation);
|
|
344
|
+
}),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
next(conversation) {
|
|
348
|
+
if (conversation.sending !== null || conversation.open > 0)
|
|
349
|
+
return;
|
|
350
|
+
const waiting = conversation.waiting.shift();
|
|
351
|
+
if (waiting === undefined) {
|
|
352
|
+
const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
|
|
353
|
+
if (conversation.starting !== step)
|
|
354
|
+
this.starts(conversation, step);
|
|
355
|
+
return;
|
|
169
356
|
}
|
|
357
|
+
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
358
|
+
conversation.sending = null;
|
|
359
|
+
this.next(conversation);
|
|
360
|
+
});
|
|
170
361
|
}
|
|
171
|
-
async
|
|
172
|
-
|
|
362
|
+
async send(conversation, waiting) {
|
|
363
|
+
const { call } = waiting;
|
|
364
|
+
let result = {};
|
|
365
|
+
let started = false;
|
|
366
|
+
let instructing = waiting.instructing;
|
|
173
367
|
const prompt = [];
|
|
174
368
|
try {
|
|
175
|
-
const route = await this.route(conversation, input.agent_id);
|
|
176
|
-
const files = input.files;
|
|
177
|
-
const paths = await placeFiles(this.kit.house, route.working_directory, files, conversation.killed.signal);
|
|
178
|
-
const lines = await this.transcribed(conversation, files, paths);
|
|
179
369
|
if (conversation.running === null) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
370
|
+
this.starts(conversation, 'session');
|
|
371
|
+
await this.launched(conversation);
|
|
372
|
+
}
|
|
373
|
+
if (conversation.running === null) {
|
|
374
|
+
const stored = conversation.session ?? call.provider_session_id;
|
|
375
|
+
const empty = call.first === true && conversation.prompted !== stored;
|
|
376
|
+
const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
|
|
377
|
+
if (call.first === true)
|
|
378
|
+
instructing = this.instructed(conversation, waiting.route);
|
|
379
|
+
started = (await launch.session) !== stored;
|
|
380
|
+
}
|
|
381
|
+
const session = conversation.running.sessionId;
|
|
382
|
+
if (started || (call.first === true && session !== call.provider_session_id)) {
|
|
383
|
+
result = { provider_session_id: session };
|
|
183
384
|
}
|
|
184
|
-
if (
|
|
185
|
-
prompt.push({
|
|
186
|
-
type: 'text',
|
|
187
|
-
text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
|
|
188
|
-
});
|
|
385
|
+
if (call.first === true) {
|
|
386
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
189
387
|
}
|
|
190
|
-
prompt.push({ type: 'text', text: String(input.text) });
|
|
191
|
-
if (lines.length > 0)
|
|
192
|
-
prompt.push({ type: 'text', text: lines.join('\n') });
|
|
193
388
|
if (conversation.kills > 0)
|
|
194
389
|
throw new Error(KILLED);
|
|
195
390
|
}
|
|
196
391
|
catch (error) {
|
|
197
|
-
|
|
392
|
+
this.starts(conversation, null);
|
|
393
|
+
if (started) {
|
|
394
|
+
conversation.session = null;
|
|
198
395
|
await this.kill(conversation);
|
|
199
|
-
|
|
396
|
+
}
|
|
397
|
+
const refused = refusal(conversation, error);
|
|
398
|
+
const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
|
|
399
|
+
waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
this.starts(conversation, null);
|
|
403
|
+
this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
|
|
404
|
+
waiting.settle(result);
|
|
405
|
+
}
|
|
406
|
+
starts(conversation, step) {
|
|
407
|
+
conversation.starting = step;
|
|
408
|
+
if (conversation.shown !== null)
|
|
409
|
+
clearTimeout(conversation.shown);
|
|
410
|
+
conversation.shown = null;
|
|
411
|
+
if (step === null || conversation.told !== null) {
|
|
412
|
+
this.tell(conversation, step);
|
|
413
|
+
return;
|
|
200
414
|
}
|
|
201
|
-
|
|
202
|
-
|
|
415
|
+
conversation.shown = setTimeout(() => {
|
|
416
|
+
conversation.shown = null;
|
|
417
|
+
if (conversation.starting === step)
|
|
418
|
+
this.tell(conversation, step);
|
|
419
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
203
420
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
421
|
+
prepares(conversation, step) {
|
|
422
|
+
conversation.preparing = step;
|
|
423
|
+
this.next(conversation);
|
|
424
|
+
}
|
|
425
|
+
tell(conversation, step) {
|
|
426
|
+
if (conversation.told === step && !conversation.untold)
|
|
427
|
+
return;
|
|
428
|
+
conversation.told = step;
|
|
429
|
+
this.retell(conversation);
|
|
430
|
+
}
|
|
431
|
+
retell(conversation) {
|
|
432
|
+
conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
|
|
433
|
+
}
|
|
434
|
+
async transcribed(conversation, messageId, text) {
|
|
435
|
+
const paths = linkedPaths(text);
|
|
436
|
+
if (paths.length === 0)
|
|
437
|
+
return [];
|
|
438
|
+
const killed = conversation.killed.signal;
|
|
439
|
+
const interrupted = conversation.interrupted.signal;
|
|
440
|
+
const transcribing = AbortSignal.any([killed, interrupted]);
|
|
441
|
+
let signal = killed;
|
|
442
|
+
const blocks = [];
|
|
443
|
+
try {
|
|
444
|
+
const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
|
|
445
|
+
const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
|
|
446
|
+
for (const path of paths) {
|
|
447
|
+
let answered = await read(path);
|
|
448
|
+
if (answered === null)
|
|
449
|
+
continue;
|
|
450
|
+
signal = transcribing;
|
|
451
|
+
this.prepares(conversation, 'transcription');
|
|
452
|
+
const said = await transcribe(async () => {
|
|
453
|
+
const address = answered ?? (await read(path));
|
|
454
|
+
answered = null;
|
|
455
|
+
if (address === null)
|
|
456
|
+
throw new Error(`${path} is no longer an audio file`);
|
|
457
|
+
return address;
|
|
458
|
+
}, signal);
|
|
459
|
+
const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
|
|
460
|
+
await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
|
|
461
|
+
blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
|
|
219
462
|
}
|
|
220
463
|
}
|
|
221
|
-
|
|
464
|
+
catch (error) {
|
|
465
|
+
if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
|
|
466
|
+
throw new NotStarted(INTERRUPTED, { cause: error });
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
return blocks;
|
|
470
|
+
}
|
|
471
|
+
bridged(conversation) {
|
|
472
|
+
if (conversation.bridge === null) {
|
|
473
|
+
const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
474
|
+
conversation.bridge = bridging;
|
|
475
|
+
bridging.catch(() => {
|
|
476
|
+
if (conversation.bridge === bridging)
|
|
477
|
+
conversation.bridge = null;
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return conversation.bridge;
|
|
481
|
+
}
|
|
482
|
+
closeBridge(conversation, bridging, bridge) {
|
|
483
|
+
bridge.close();
|
|
484
|
+
if (conversation.bridge === bridging)
|
|
485
|
+
conversation.bridge = null;
|
|
486
|
+
}
|
|
487
|
+
closeIdleBridge(conversation) {
|
|
488
|
+
const bridging = conversation.bridge;
|
|
489
|
+
if (bridging === null || conversation.running !== null || conversation.launch !== null)
|
|
490
|
+
return;
|
|
491
|
+
conversation.bridge = null;
|
|
492
|
+
void bridging.then((bridge) => bridge.close(), () => undefined);
|
|
222
493
|
}
|
|
223
494
|
async interrupt(conversation, turnId) {
|
|
224
495
|
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
@@ -229,11 +500,13 @@ export class Conversations {
|
|
|
229
500
|
}
|
|
230
501
|
halt(conversation) {
|
|
231
502
|
if (conversation.opening !== null)
|
|
232
|
-
|
|
503
|
+
killMarked(marker(conversation.opening));
|
|
504
|
+
for (const scope of conversation.leftovers)
|
|
505
|
+
scope.kill();
|
|
233
506
|
if (conversation.running === null)
|
|
234
507
|
return;
|
|
235
508
|
conversation.running.killed = true;
|
|
236
|
-
|
|
509
|
+
conversation.running.scope.kill();
|
|
237
510
|
}
|
|
238
511
|
async kill(conversation) {
|
|
239
512
|
const running = conversation.running;
|
|
@@ -245,6 +518,7 @@ export class Conversations {
|
|
|
245
518
|
if (running === null)
|
|
246
519
|
return;
|
|
247
520
|
try {
|
|
521
|
+
running.reader.requested(option, value);
|
|
248
522
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
249
523
|
sessionId: running.sessionId,
|
|
250
524
|
configId: option,
|
|
@@ -253,6 +527,7 @@ export class Conversations {
|
|
|
253
527
|
this.options(conversation, answered.configOptions);
|
|
254
528
|
}
|
|
255
529
|
catch (error) {
|
|
530
|
+
running.reader.refused(option);
|
|
256
531
|
logged(error);
|
|
257
532
|
}
|
|
258
533
|
}
|
|
@@ -269,57 +544,78 @@ export class Conversations {
|
|
|
269
544
|
}
|
|
270
545
|
return route;
|
|
271
546
|
}
|
|
272
|
-
async start(conversation,
|
|
547
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
548
|
+
const route = await routed;
|
|
549
|
+
const reader = readerFor(route.kind);
|
|
550
|
+
reader.session = sessionId;
|
|
273
551
|
const app = client({ name: '@agentshouse/kit' })
|
|
274
|
-
.onNotification('session/update', ({ params }) => this.update(conversation, params))
|
|
275
|
-
.onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
|
|
276
|
-
.onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
|
|
277
552
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
278
|
-
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
553
|
+
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
554
|
+
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
|
|
279
555
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
280
|
-
const
|
|
556
|
+
const bridging = this.bridged(conversation);
|
|
557
|
+
const bridge = await step(unbridged, bridging);
|
|
281
558
|
if (conversation.kills > 0) {
|
|
282
|
-
|
|
559
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
283
560
|
throw new Error(KILLED);
|
|
284
561
|
}
|
|
285
562
|
conversation.opening = bridge;
|
|
563
|
+
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
564
|
+
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
565
|
+
let adapter = null;
|
|
286
566
|
try {
|
|
287
567
|
await unlessAborted(createHowWeWork(bridge), conversation.killed.signal);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
568
|
+
bridged(bridge);
|
|
569
|
+
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
570
|
+
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
571
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
572
|
+
work.read(message);
|
|
573
|
+
this.read(conversation, reader, message);
|
|
574
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
575
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
291
576
|
const agent = adapter.connection.agent;
|
|
292
577
|
const cwd = route.working_directory;
|
|
293
|
-
const _meta = CLIS[route.kind].sessionMeta;
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
};
|
|
305
|
-
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
578
|
+
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
579
|
+
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
580
|
+
const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
581
|
+
reader.session = opened.sessionId;
|
|
582
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
583
|
+
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
584
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
585
|
+
}
|
|
586
|
+
const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
587
|
+
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
588
|
+
void work.settled().then(() => scope.record());
|
|
306
589
|
conversation.running = running;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
this.
|
|
311
|
-
void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
|
|
590
|
+
conversation.session = running.sessionId;
|
|
591
|
+
conversation.options = options;
|
|
592
|
+
this.facts(conversation);
|
|
593
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
312
594
|
return running.sessionId;
|
|
313
595
|
}
|
|
314
596
|
catch (error) {
|
|
315
|
-
|
|
316
|
-
|
|
597
|
+
adapter?.retire();
|
|
598
|
+
work.close();
|
|
599
|
+
conversation.leftovers.add(scope);
|
|
600
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
317
601
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
318
602
|
}
|
|
319
603
|
finally {
|
|
320
604
|
conversation.opening = null;
|
|
321
605
|
}
|
|
322
606
|
}
|
|
607
|
+
async resume(conversation, kind, adapter, request, empty) {
|
|
608
|
+
try {
|
|
609
|
+
return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
|
|
613
|
+
throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
|
|
614
|
+
}
|
|
615
|
+
logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
323
619
|
async launchSettings(running, route, offered) {
|
|
324
620
|
let options = offered;
|
|
325
621
|
const settings = [
|
|
@@ -331,6 +627,7 @@ export class Conversations {
|
|
|
331
627
|
if (value === null)
|
|
332
628
|
continue;
|
|
333
629
|
const option = options.find((candidate) => candidate.category === category);
|
|
630
|
+
running.reader.requested(option?.id ?? category, value);
|
|
334
631
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
335
632
|
sessionId: running.sessionId,
|
|
336
633
|
configId: option?.id ?? category,
|
|
@@ -340,23 +637,37 @@ export class Conversations {
|
|
|
340
637
|
}
|
|
341
638
|
return options;
|
|
342
639
|
}
|
|
343
|
-
exited(conversation,
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
640
|
+
exited(conversation, running, cause) {
|
|
641
|
+
this.detach(conversation, running);
|
|
642
|
+
if (conversation.turn !== null) {
|
|
643
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
644
|
+
}
|
|
645
|
+
this.next(conversation);
|
|
646
|
+
this.kit.changed();
|
|
647
|
+
}
|
|
648
|
+
detach(conversation, running) {
|
|
649
|
+
if (conversation.bridge === running.bridging)
|
|
650
|
+
conversation.bridge = null;
|
|
651
|
+
running.work.close();
|
|
652
|
+
conversation.leftovers.add(running.scope);
|
|
347
653
|
conversation.running = null;
|
|
348
654
|
conversation.commands = null;
|
|
349
|
-
conversation.
|
|
350
|
-
conversation.queued.length = 0;
|
|
655
|
+
conversation.options = null;
|
|
351
656
|
for (const [id, question] of this.questions) {
|
|
352
657
|
if (question.conversation === conversation)
|
|
353
658
|
this.questions.delete(id);
|
|
354
659
|
}
|
|
355
|
-
this.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
|
|
660
|
+
this.process(conversation, false);
|
|
661
|
+
}
|
|
662
|
+
process(conversation, running) {
|
|
663
|
+
conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
|
|
664
|
+
}
|
|
665
|
+
facts(conversation) {
|
|
666
|
+
this.process(conversation, true);
|
|
667
|
+
if (conversation.commands !== null)
|
|
668
|
+
this.commands(conversation, conversation.commands);
|
|
669
|
+
if (conversation.options !== null)
|
|
670
|
+
this.options(conversation, conversation.options);
|
|
360
671
|
}
|
|
361
672
|
commands(conversation, commands) {
|
|
362
673
|
conversation.commands = commands;
|
|
@@ -367,11 +678,12 @@ export class Conversations {
|
|
|
367
678
|
options(conversation, options) {
|
|
368
679
|
if (conversation.running === null)
|
|
369
680
|
return;
|
|
681
|
+
conversation.options = options;
|
|
370
682
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
371
683
|
}
|
|
372
684
|
deliver(conversation, turn, event, body) {
|
|
373
685
|
this.reporting++;
|
|
374
|
-
const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
|
|
686
|
+
const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
|
|
375
687
|
conversation.reports = delivered
|
|
376
688
|
.catch(() => undefined)
|
|
377
689
|
.finally(() => {
|
|
@@ -380,119 +692,95 @@ export class Conversations {
|
|
|
380
692
|
});
|
|
381
693
|
return delivered;
|
|
382
694
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
.then(() => this.admitted(conversation, turn))
|
|
388
|
-
.catch(logged);
|
|
389
|
-
return turn;
|
|
695
|
+
write(conversation, turn, operation) {
|
|
696
|
+
if (turn.ended || !this.watched)
|
|
697
|
+
return;
|
|
698
|
+
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
390
699
|
}
|
|
391
|
-
|
|
392
|
-
turn.
|
|
393
|
-
|
|
394
|
-
this.kit.send(note);
|
|
395
|
-
if (turn.text !== '') {
|
|
396
|
-
turn.unsentFrom = 0;
|
|
397
|
-
this.draft(conversation, turn);
|
|
398
|
-
}
|
|
399
|
-
if (turn.entries !== null)
|
|
400
|
-
this.plan(conversation, turn);
|
|
700
|
+
replay(conversation, turn) {
|
|
701
|
+
for (const operation of turn.parts.operations())
|
|
702
|
+
this.write(conversation, turn, operation);
|
|
401
703
|
}
|
|
402
704
|
begin(conversation) {
|
|
403
|
-
const turn = this.
|
|
705
|
+
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
706
|
+
conversation.open++;
|
|
707
|
+
void this.deliver(conversation, turn, 'started', {}).catch(logged);
|
|
404
708
|
conversation.turn = turn;
|
|
709
|
+
if (conversation.context !== null)
|
|
710
|
+
turn.parts.measure(conversation.context);
|
|
711
|
+
for (const part of conversation.held.splice(0))
|
|
712
|
+
turn.parts.start(part);
|
|
405
713
|
this.kit.changed();
|
|
406
714
|
return turn;
|
|
407
715
|
}
|
|
408
|
-
|
|
409
|
-
if (turn.ended)
|
|
410
|
-
return;
|
|
411
|
-
const segment = turn.segment;
|
|
412
|
-
turn.segment = null;
|
|
413
|
-
if (segment?.phase === 'note')
|
|
414
|
-
this.note(conversation, turn, segment.text);
|
|
415
|
-
else if (segment !== null)
|
|
416
|
-
this.answer(conversation, turn, segment.text);
|
|
417
|
-
this.end(conversation, turn, { text: turn.text });
|
|
418
|
-
}
|
|
419
|
-
end(conversation, turn, outcome) {
|
|
716
|
+
end(conversation, turn, failed, notice = null) {
|
|
420
717
|
if (turn.ended)
|
|
421
718
|
return;
|
|
719
|
+
if (failed !== null)
|
|
720
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
721
|
+
if (conversation.turn === turn) {
|
|
722
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
723
|
+
this.apply(conversation, turn, event);
|
|
724
|
+
}
|
|
725
|
+
if (notice !== null)
|
|
726
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
422
727
|
turn.ended = true;
|
|
423
728
|
conversation.open--;
|
|
424
729
|
if (conversation.turn === turn)
|
|
425
730
|
conversation.turn = null;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
731
|
+
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
732
|
+
parts: turn.parts.saved(),
|
|
733
|
+
context: turn.parts.context,
|
|
734
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
735
|
+
}));
|
|
736
|
+
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
737
|
+
this.next(conversation);
|
|
430
738
|
this.kit.changed();
|
|
431
739
|
}
|
|
432
|
-
|
|
433
|
-
if (conversation.open > 0)
|
|
434
|
-
conversation.late.push(cause);
|
|
435
|
-
else
|
|
436
|
-
this.end(conversation, this.report(conversation), { failed: cause });
|
|
437
|
-
}
|
|
438
|
-
prompt(conversation, prompt) {
|
|
740
|
+
prompt(conversation, prompt, taken) {
|
|
439
741
|
const running = conversation.running;
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
this.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
else
|
|
451
|
-
this.end(conversation,
|
|
742
|
+
const turn = this.begin(conversation);
|
|
743
|
+
turn.prompted = true;
|
|
744
|
+
turn.taken = taken;
|
|
745
|
+
conversation.prompted = running.sessionId;
|
|
746
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
747
|
+
this.took(turn);
|
|
748
|
+
const notice = answeredFailure(answer);
|
|
749
|
+
if (notice !== null) {
|
|
750
|
+
this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
754
|
+
}
|
|
755
|
+
}, async (error) => {
|
|
756
|
+
if (running.adapter.connection.signal.aborted) {
|
|
757
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
this.took(turn);
|
|
761
|
+
if (conversation.open === 1 && conversation.running === running) {
|
|
762
|
+
running.adapter.retire();
|
|
763
|
+
this.detach(conversation, running);
|
|
764
|
+
}
|
|
765
|
+
this.end(conversation, turn, answeredWith(error));
|
|
452
766
|
}
|
|
453
767
|
});
|
|
454
768
|
}
|
|
455
|
-
attach(turn, sent) {
|
|
456
|
-
turn.prompt = sent;
|
|
457
|
-
sent.turn = turn;
|
|
458
|
-
}
|
|
459
|
-
answered(conversation, sent, failed) {
|
|
460
|
-
const turn = sent.turn;
|
|
461
|
-
if (turn === null) {
|
|
462
|
-
const queued = conversation.queued.indexOf(sent);
|
|
463
|
-
if (queued < 0)
|
|
464
|
-
return;
|
|
465
|
-
conversation.queued.splice(queued, 1);
|
|
466
|
-
if (failed !== null)
|
|
467
|
-
this.failed(conversation, failed);
|
|
468
|
-
}
|
|
469
|
-
else if (failed !== null) {
|
|
470
|
-
if (turn.ended)
|
|
471
|
-
this.failed(conversation, failed);
|
|
472
|
-
else
|
|
473
|
-
this.end(conversation, turn, { failed });
|
|
474
|
-
}
|
|
475
|
-
else if (turn.prompt === sent) {
|
|
476
|
-
this.finish(conversation, turn);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
769
|
started(conversation) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const turn = this.begin(conversation);
|
|
483
|
-
const queued = conversation.queued.shift();
|
|
484
|
-
if (queued !== undefined)
|
|
485
|
-
this.attach(turn, queued);
|
|
770
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
771
|
+
this.took(turn);
|
|
486
772
|
return turn;
|
|
487
773
|
}
|
|
488
774
|
ended(conversation) {
|
|
489
775
|
const turn = conversation.turn;
|
|
490
776
|
if (turn === null)
|
|
491
777
|
return;
|
|
492
|
-
if (turn.
|
|
493
|
-
this.
|
|
778
|
+
if (!turn.prompted) {
|
|
779
|
+
this.end(conversation, turn, null);
|
|
494
780
|
}
|
|
495
781
|
else {
|
|
782
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
783
|
+
this.apply(conversation, turn, event);
|
|
496
784
|
conversation.turn = null;
|
|
497
785
|
this.kit.changed();
|
|
498
786
|
}
|
|
@@ -541,101 +829,110 @@ export class Conversations {
|
|
|
541
829
|
if (content !== null)
|
|
542
830
|
this.questions.get(interactionId)?.answer({ action: 'accept', content });
|
|
543
831
|
}
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
832
|
+
read(conversation, reader, message) {
|
|
833
|
+
for (const event of reader.read(message))
|
|
834
|
+
this.event(conversation, event);
|
|
835
|
+
}
|
|
836
|
+
event(conversation, event) {
|
|
837
|
+
switch (event.kind) {
|
|
838
|
+
case 'started':
|
|
839
|
+
this.started(conversation);
|
|
840
|
+
return;
|
|
841
|
+
case 'ended':
|
|
842
|
+
this.ended(conversation);
|
|
843
|
+
return;
|
|
844
|
+
case 'commands':
|
|
845
|
+
this.commands(conversation, event.commands);
|
|
846
|
+
return;
|
|
847
|
+
case 'options':
|
|
848
|
+
this.options(conversation, event.options);
|
|
849
|
+
return;
|
|
850
|
+
case 'context':
|
|
851
|
+
conversation.context = { used: event.used, window: event.window };
|
|
852
|
+
conversation.turn?.parts.measure(conversation.context);
|
|
853
|
+
return;
|
|
854
|
+
case 'subagent-done':
|
|
855
|
+
this.subagentDone(conversation, event.id, event.result);
|
|
856
|
+
return;
|
|
857
|
+
case 'marker':
|
|
858
|
+
case 'image':
|
|
859
|
+
if (conversation.turn !== null)
|
|
860
|
+
break;
|
|
861
|
+
if (event.kind === 'marker' && event.marker === 'job')
|
|
862
|
+
conversation.held.push({ type: 'marker', marker: 'job', text: event.text });
|
|
863
|
+
return;
|
|
568
864
|
}
|
|
865
|
+
this.apply(conversation, this.started(conversation), event);
|
|
569
866
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
conversation.jobs.delete(job.id);
|
|
575
|
-
this.kit.changed();
|
|
867
|
+
took(turn) {
|
|
868
|
+
const taken = turn.taken;
|
|
869
|
+
turn.taken = null;
|
|
870
|
+
taken?.();
|
|
576
871
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
872
|
+
apply(conversation, turn, event) {
|
|
873
|
+
switch (event.kind) {
|
|
874
|
+
case 'text':
|
|
875
|
+
case 'reasoning':
|
|
876
|
+
turn.parts.write(event.kind, event.id, event.text);
|
|
877
|
+
return;
|
|
878
|
+
case 'command':
|
|
879
|
+
turn.parts.command();
|
|
880
|
+
return;
|
|
881
|
+
case 'subagent': {
|
|
882
|
+
const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
|
|
883
|
+
conversation.subagents.set(event.id, { description: event.description, turn, index });
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
case 'message':
|
|
887
|
+
turn.parts.start({ type: 'message', command: event.command, target: event.target });
|
|
888
|
+
return;
|
|
889
|
+
case 'marker':
|
|
890
|
+
turn.parts.start({ type: 'marker', marker: event.marker, text: event.text });
|
|
891
|
+
return;
|
|
892
|
+
case 'plan':
|
|
893
|
+
turn.parts.plan(event.entries);
|
|
894
|
+
return;
|
|
895
|
+
case 'image':
|
|
896
|
+
turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
|
|
897
|
+
this.kit.changed();
|
|
898
|
+
return;
|
|
585
899
|
}
|
|
586
|
-
turn.segment ??= { messageId, phase, text: '' };
|
|
587
|
-
turn.segment.text += text;
|
|
588
900
|
}
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
if (
|
|
901
|
+
subagentDone(conversation, id, result) {
|
|
902
|
+
const subagent = conversation.subagents.get(id);
|
|
903
|
+
if (subagent === undefined)
|
|
592
904
|
return;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
note(conversation, turn, text) {
|
|
597
|
-
if (text.trim() === '')
|
|
905
|
+
conversation.subagents.delete(id);
|
|
906
|
+
if (subagent.turn === conversation.turn) {
|
|
907
|
+
subagent.turn.parts.done(subagent.index, result);
|
|
598
908
|
return;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
909
|
+
}
|
|
910
|
+
const part = { type: 'subagent', description: subagent.description, status: 'done', result };
|
|
911
|
+
if (conversation.turn === null)
|
|
912
|
+
conversation.held.push(part);
|
|
602
913
|
else
|
|
603
|
-
turn.
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
turn.draftSequence++;
|
|
622
|
-
turn.sent = blocks;
|
|
623
|
-
turn.unsentFrom = null;
|
|
914
|
+
conversation.turn.parts.start(part);
|
|
915
|
+
}
|
|
916
|
+
async image(conversation, turn, reserved, data, mediaType) {
|
|
917
|
+
const bytes = Buffer.from(data, 'base64');
|
|
918
|
+
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
919
|
+
try {
|
|
920
|
+
const declared = (await this.deliver(conversation, turn, 'images', {
|
|
921
|
+
name,
|
|
922
|
+
media_type: mediaType,
|
|
923
|
+
bytes: bytes.length,
|
|
924
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
925
|
+
}));
|
|
926
|
+
const saved = await uploaded(declared.upload, {
|
|
927
|
+
bytes: bytes.length,
|
|
928
|
+
send: (url, start, end) => sentPart(url, bytes.subarray(start, end)),
|
|
929
|
+
finish: (upload) => this.kit.house.deliver('/kit/uploads/finish', { upload }),
|
|
930
|
+
});
|
|
931
|
+
turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
|
|
624
932
|
}
|
|
625
|
-
|
|
626
|
-
|
|
933
|
+
catch (error) {
|
|
934
|
+
const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
935
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
|
|
627
936
|
}
|
|
628
937
|
}
|
|
629
|
-
plan(conversation, turn) {
|
|
630
|
-
const sent = this.kit.send({
|
|
631
|
-
type: 'plan',
|
|
632
|
-
conversation_id: conversation.id,
|
|
633
|
-
turn_id: turn.id,
|
|
634
|
-
sequence: turn.planSequence + 1,
|
|
635
|
-
from: 0,
|
|
636
|
-
steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
|
|
637
|
-
});
|
|
638
|
-
if (sent)
|
|
639
|
-
turn.planSequence++;
|
|
640
|
-
}
|
|
641
938
|
}
|