@agentshouse/kit 0.2.1-cca9e1f43de44 → 0.2.1-ccb257fa8a318
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 +42 -40
- package/dist/acp.js +92 -115
- package/dist/agents.js +112 -24
- package/dist/api.js +10 -6
- package/dist/app-git.js +10 -26
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +11 -25
- 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 +131 -27
- package/dist/codex-main.js +58 -0
- package/dist/connection.js +116 -0
- package/dist/conversations.js +523 -202
- package/dist/copies.js +185 -123
- package/dist/end.js +24 -5
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +16 -78
- 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 -177
- package/dist/instructions.js +8 -10
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -22
- package/dist/login.js +118 -107
- package/dist/logout.js +14 -16
- 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 +118 -51
- package/dist/scope.js +206 -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 +23 -40
- 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 +346 -0
- 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
|
@@ -1,44 +1,121 @@
|
|
|
1
|
-
import { client, } from '@agentclientprotocol/sdk';
|
|
1
|
+
import { client, RequestError, } from '@agentclientprotocol/sdk';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { unlessAborted } from './abort.js';
|
|
6
|
-
import {
|
|
6
|
+
import { 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';
|
|
17
|
+
import { killMarked, Scope } from './scope.js';
|
|
14
18
|
import { holdSecretInput } from './secret-input.js';
|
|
15
|
-
import { createHowWeWork } from './skills.js';
|
|
16
19
|
import { transcribe } from './transcription.js';
|
|
20
|
+
import { workFor } from './work.js';
|
|
21
|
+
const SIGNED_OUT = RequestError.authRequired().code;
|
|
17
22
|
const KILLED = 'the conversation was killed';
|
|
18
|
-
|
|
19
|
-
const
|
|
23
|
+
const INTERRUPTED = 'the conversation was interrupted';
|
|
24
|
+
const REFUSED = 'the CLI refused the prompt';
|
|
25
|
+
const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
|
|
26
|
+
const GONE = 'House no longer holds this conversation; start a new one';
|
|
27
|
+
const RETIRED = "this conversation's Agent is retired, so it cannot start";
|
|
28
|
+
const ENDED = {
|
|
29
|
+
conversation_not_found: GONE,
|
|
30
|
+
conversation_historical: RETIRED,
|
|
31
|
+
};
|
|
32
|
+
const NO_START = "the Agent's CLI did not start";
|
|
33
|
+
const NO_SESSION = "the Agent's CLI did not open a session";
|
|
34
|
+
const NO_RESUME = "the Agent's CLI could not resume this conversation";
|
|
35
|
+
const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
|
|
36
|
+
const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
|
|
37
|
+
const UNSTEERED = "the Agent's CLI did not take the message into its running turn";
|
|
38
|
+
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', 'done', 'option', 'answer']);
|
|
20
70
|
function causeOf(error) {
|
|
21
71
|
return error instanceof Error ? error.message : String(error);
|
|
22
72
|
}
|
|
73
|
+
function answeredWith(error) {
|
|
74
|
+
return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
75
|
+
}
|
|
76
|
+
function exitedWith(running, cause) {
|
|
77
|
+
if (running.killed)
|
|
78
|
+
return { failure: 'killed', cause: KILLED };
|
|
79
|
+
return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
|
|
80
|
+
}
|
|
23
81
|
function answerMessage(request, response) {
|
|
24
82
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
25
83
|
}
|
|
84
|
+
async function sentPart(url, bytes, headers) {
|
|
85
|
+
const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
|
|
86
|
+
await answer.arrayBuffer();
|
|
87
|
+
if (!answer.ok)
|
|
88
|
+
throw storeRefusal('upload', answer.status);
|
|
89
|
+
}
|
|
26
90
|
function logged(error) {
|
|
27
91
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
28
92
|
}
|
|
93
|
+
function refusal(conversation, error) {
|
|
94
|
+
if (conversation.kills > 0)
|
|
95
|
+
return KILLED;
|
|
96
|
+
const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
|
|
97
|
+
logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
|
|
98
|
+
return causeOf(error);
|
|
99
|
+
}
|
|
100
|
+
function audioOf(result) {
|
|
101
|
+
if (result.isError === true)
|
|
102
|
+
return null;
|
|
103
|
+
const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
|
|
104
|
+
return file?.media_type?.startsWith('audio/') === true ? file : null;
|
|
105
|
+
}
|
|
29
106
|
function marker(bridge) {
|
|
30
107
|
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
31
108
|
}
|
|
32
|
-
function
|
|
33
|
-
|
|
109
|
+
function steers(adapter) {
|
|
110
|
+
return adapter.initialized._meta?.steering?.supported === true;
|
|
34
111
|
}
|
|
35
112
|
class Turn {
|
|
36
113
|
id = randomUUID();
|
|
37
114
|
parts;
|
|
38
|
-
admitted = false;
|
|
39
115
|
questions = 0;
|
|
40
116
|
secrets = 0;
|
|
41
117
|
prompted = false;
|
|
118
|
+
taken = null;
|
|
42
119
|
ended = false;
|
|
43
120
|
images = 0;
|
|
44
121
|
uploads = [];
|
|
@@ -52,13 +129,19 @@ class Conversation {
|
|
|
52
129
|
session = null;
|
|
53
130
|
turn = null;
|
|
54
131
|
commands = null;
|
|
132
|
+
options = null;
|
|
133
|
+
exitUntold = false;
|
|
55
134
|
context = null;
|
|
56
|
-
|
|
135
|
+
leftovers = new Set();
|
|
57
136
|
waiting = [];
|
|
137
|
+
done = false;
|
|
58
138
|
sending = null;
|
|
59
139
|
subagents = new Map();
|
|
60
140
|
held = [];
|
|
61
141
|
opening = null;
|
|
142
|
+
bridge = null;
|
|
143
|
+
launch = null;
|
|
144
|
+
prompted = null;
|
|
62
145
|
kills = 0;
|
|
63
146
|
killed = new AbortController();
|
|
64
147
|
interrupts = 0;
|
|
@@ -66,6 +149,11 @@ class Conversation {
|
|
|
66
149
|
open = 0;
|
|
67
150
|
queue = Promise.resolve();
|
|
68
151
|
reports = Promise.resolve();
|
|
152
|
+
starting = null;
|
|
153
|
+
preparing = null;
|
|
154
|
+
told = null;
|
|
155
|
+
untold = false;
|
|
156
|
+
shown = null;
|
|
69
157
|
constructor(id) {
|
|
70
158
|
this.id = id;
|
|
71
159
|
}
|
|
@@ -81,70 +169,71 @@ function asksSecret(request) {
|
|
|
81
169
|
export class Conversations {
|
|
82
170
|
kit;
|
|
83
171
|
conversations = new Map();
|
|
84
|
-
acks = new Map();
|
|
85
172
|
questions = new Map();
|
|
86
|
-
carrying = 0;
|
|
87
173
|
reporting = 0;
|
|
88
174
|
watched = false;
|
|
89
|
-
recheck = null;
|
|
90
175
|
constructor(kit) {
|
|
91
176
|
this.kit = kit;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.acks.set(input.input_id, work);
|
|
113
|
-
return work;
|
|
114
|
-
})();
|
|
115
|
-
carried
|
|
116
|
-
.then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
|
|
117
|
-
.catch(logged)
|
|
118
|
-
.finally(() => {
|
|
119
|
-
this.carrying--;
|
|
120
|
-
this.kit.changed();
|
|
177
|
+
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
178
|
+
setInterval(() => this.look(), 2_000);
|
|
179
|
+
}
|
|
180
|
+
call(call) {
|
|
181
|
+
void this.kit.calls.carry(call.input_id, () => {
|
|
182
|
+
const conversation = this.conversation(call.conversation_id);
|
|
183
|
+
if (call.kind === 'kill') {
|
|
184
|
+
conversation.kills++;
|
|
185
|
+
conversation.killed.abort(new Error(KILLED));
|
|
186
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
187
|
+
waiting.settle({ refused: KILLED });
|
|
188
|
+
this.halt(conversation);
|
|
189
|
+
}
|
|
190
|
+
if (call.kind === 'interrupt') {
|
|
191
|
+
conversation.interrupts++;
|
|
192
|
+
conversation.interrupted.abort();
|
|
193
|
+
}
|
|
194
|
+
const handled = conversation.queue.then(() => this.carry(conversation, call));
|
|
195
|
+
conversation.queue = handled.catch(() => undefined);
|
|
196
|
+
return handled.then(async ({ result }) => ({ result: await result }));
|
|
121
197
|
});
|
|
122
198
|
}
|
|
123
199
|
idle() {
|
|
124
|
-
if (this.
|
|
200
|
+
if (this.reporting > 0)
|
|
125
201
|
return false;
|
|
126
202
|
for (const conversation of this.conversations.values()) {
|
|
127
203
|
const turn = conversation.turn;
|
|
128
204
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
129
|
-
if (conversation.
|
|
205
|
+
if (conversation.open > (asking ? 1 : 0))
|
|
130
206
|
return false;
|
|
131
207
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (running === null || running.baseline === null)
|
|
135
|
-
continue;
|
|
136
|
-
if ([...processes(marker(running.bridge))].every((found) => running.baseline.has(found)))
|
|
137
|
-
continue;
|
|
138
|
-
this.recheck ??= setTimeout(() => {
|
|
139
|
-
this.recheck = null;
|
|
140
|
-
this.kit.changed();
|
|
141
|
-
}, PROCESS_RECHECK_MS);
|
|
208
|
+
const conversations = [...this.conversations.values()];
|
|
209
|
+
if (conversations.some((conversation) => conversation.running?.work.busy()))
|
|
142
210
|
return false;
|
|
211
|
+
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
212
|
+
}
|
|
213
|
+
quiet() {
|
|
214
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
|
|
215
|
+
}
|
|
216
|
+
look() {
|
|
217
|
+
for (const conversation of this.conversations.values()) {
|
|
218
|
+
for (const scope of conversation.leftovers) {
|
|
219
|
+
if (!scope.empty())
|
|
220
|
+
continue;
|
|
221
|
+
scope.close();
|
|
222
|
+
conversation.leftovers.delete(scope);
|
|
223
|
+
}
|
|
143
224
|
}
|
|
144
|
-
|
|
225
|
+
this.kit.changed();
|
|
145
226
|
}
|
|
146
227
|
opened() {
|
|
147
228
|
this.watched = false;
|
|
229
|
+
for (const conversation of this.conversations.values()) {
|
|
230
|
+
if (conversation.running !== null)
|
|
231
|
+
this.facts(conversation);
|
|
232
|
+
else if (conversation.exitUntold)
|
|
233
|
+
this.process(conversation, false);
|
|
234
|
+
if (conversation.untold || conversation.told !== null)
|
|
235
|
+
this.retell(conversation);
|
|
236
|
+
}
|
|
148
237
|
}
|
|
149
238
|
watching(watched) {
|
|
150
239
|
const resend = watched && !this.watched;
|
|
@@ -164,127 +253,309 @@ export class Conversations {
|
|
|
164
253
|
}
|
|
165
254
|
return conversation;
|
|
166
255
|
}
|
|
167
|
-
async carry(conversation,
|
|
168
|
-
if (
|
|
169
|
-
return
|
|
170
|
-
if (
|
|
171
|
-
return this.message(conversation,
|
|
172
|
-
if (
|
|
256
|
+
async carry(conversation, call) {
|
|
257
|
+
if (call.kind === 'open')
|
|
258
|
+
return this.open(conversation, call);
|
|
259
|
+
if (call.kind === 'message')
|
|
260
|
+
return this.message(conversation, call);
|
|
261
|
+
if (call.kind === 'done') {
|
|
262
|
+
conversation.done = true;
|
|
263
|
+
this.next(conversation);
|
|
264
|
+
return { result: Promise.resolve({}) };
|
|
265
|
+
}
|
|
266
|
+
await this.launched(conversation);
|
|
267
|
+
if (call.kind === 'interrupt') {
|
|
173
268
|
if (--conversation.interrupts === 0)
|
|
174
269
|
conversation.interrupted = new AbortController();
|
|
175
|
-
await this.interrupt(conversation, String(
|
|
270
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
176
271
|
}
|
|
177
|
-
if (
|
|
272
|
+
if (call.kind === 'kill') {
|
|
178
273
|
await this.kill(conversation);
|
|
179
274
|
await conversation.sending;
|
|
180
275
|
if (--conversation.kills === 0)
|
|
181
276
|
conversation.killed = new AbortController();
|
|
182
277
|
}
|
|
183
|
-
if (
|
|
184
|
-
await this.option(conversation, String(
|
|
185
|
-
if (
|
|
186
|
-
const question = this.questions.get(String(
|
|
278
|
+
if (call.kind === 'option')
|
|
279
|
+
await this.option(conversation, String(call.option), call.value);
|
|
280
|
+
if (call.kind === 'answer') {
|
|
281
|
+
const question = this.questions.get(String(call.interaction_id));
|
|
187
282
|
if (question === undefined) {
|
|
188
|
-
const text = answerMessage(
|
|
189
|
-
return this.message(conversation, { ...
|
|
283
|
+
const text = answerMessage(call.request, call.response);
|
|
284
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
190
285
|
}
|
|
191
|
-
question.answer(
|
|
286
|
+
question.answer(call.response);
|
|
192
287
|
}
|
|
193
|
-
return {
|
|
288
|
+
return { result: Promise.resolve({}) };
|
|
289
|
+
}
|
|
290
|
+
async open(conversation, call) {
|
|
291
|
+
await this.stop(conversation);
|
|
292
|
+
const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
|
|
293
|
+
return {
|
|
294
|
+
result: launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) })),
|
|
295
|
+
};
|
|
194
296
|
}
|
|
195
|
-
async
|
|
196
|
-
|
|
197
|
-
|
|
297
|
+
async stop(conversation) {
|
|
298
|
+
const running = conversation.running;
|
|
299
|
+
running?.scope.kill();
|
|
300
|
+
await running?.adapter.exited;
|
|
301
|
+
}
|
|
302
|
+
launch(conversation, route, sessionId, empty) {
|
|
303
|
+
let opened;
|
|
304
|
+
let failed;
|
|
305
|
+
const bridge = new Promise((resolve, reject) => {
|
|
306
|
+
opened = resolve;
|
|
307
|
+
failed = reject;
|
|
308
|
+
});
|
|
309
|
+
bridge.catch(() => undefined);
|
|
310
|
+
const session = this.start(conversation, route, sessionId, empty, opened);
|
|
311
|
+
session.catch(failed);
|
|
312
|
+
const launch = { bridge, session };
|
|
313
|
+
conversation.launch = launch;
|
|
314
|
+
void session
|
|
315
|
+
.catch(() => undefined)
|
|
316
|
+
.then(() => {
|
|
317
|
+
if (conversation.launch !== launch)
|
|
318
|
+
return;
|
|
319
|
+
conversation.launch = null;
|
|
320
|
+
this.closeIdleBridge(conversation);
|
|
321
|
+
this.next(conversation);
|
|
322
|
+
});
|
|
323
|
+
this.next(conversation);
|
|
324
|
+
return launch;
|
|
325
|
+
}
|
|
326
|
+
async launched(conversation) {
|
|
327
|
+
while (conversation.launch !== null)
|
|
328
|
+
await conversation.launch.session.catch(() => undefined);
|
|
329
|
+
}
|
|
330
|
+
instructed(conversation, route) {
|
|
331
|
+
const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
|
|
332
|
+
if (bridge === undefined)
|
|
333
|
+
return null;
|
|
334
|
+
const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
|
|
335
|
+
read.catch(() => undefined);
|
|
336
|
+
return read;
|
|
337
|
+
}
|
|
338
|
+
async message(conversation, call) {
|
|
339
|
+
const content = [
|
|
340
|
+
...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
|
|
341
|
+
{ type: 'text', text: String(call.text) },
|
|
342
|
+
];
|
|
343
|
+
let route;
|
|
344
|
+
let instructing = null;
|
|
198
345
|
try {
|
|
199
|
-
|
|
346
|
+
route = await this.route(conversation, call.agent_id);
|
|
347
|
+
if (call.first === true)
|
|
348
|
+
instructing = this.instructed(conversation, route);
|
|
349
|
+
if (call.message_id !== null)
|
|
350
|
+
content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
|
|
351
|
+
if (conversation.kills > 0)
|
|
352
|
+
throw new Error(KILLED);
|
|
200
353
|
}
|
|
201
354
|
catch (error) {
|
|
202
|
-
|
|
355
|
+
this.closeIdleBridge(conversation);
|
|
356
|
+
this.prepares(conversation, null);
|
|
357
|
+
return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
358
|
+
}
|
|
359
|
+
conversation.preparing = null;
|
|
360
|
+
const { promise, resolve: settle } = Promise.withResolvers();
|
|
361
|
+
const waiting = { call, route, content, instructing, settle };
|
|
362
|
+
if (call.at_once === true && conversation.open > 0)
|
|
363
|
+
await this.atOnce(conversation, waiting);
|
|
364
|
+
else
|
|
365
|
+
conversation.waiting.push(waiting);
|
|
366
|
+
this.next(conversation);
|
|
367
|
+
return { result: promise };
|
|
368
|
+
}
|
|
369
|
+
async atOnce(conversation, waiting) {
|
|
370
|
+
while (conversation.sending !== null)
|
|
371
|
+
await conversation.sending;
|
|
372
|
+
const running = conversation.running;
|
|
373
|
+
if (running !== null && conversation.open > 0 && steers(running.adapter)) {
|
|
374
|
+
conversation.sending = this.steer(conversation, running, waiting);
|
|
375
|
+
await conversation.sending;
|
|
376
|
+
conversation.sending = null;
|
|
377
|
+
return;
|
|
203
378
|
}
|
|
379
|
+
this.ahead(conversation, waiting);
|
|
380
|
+
if (conversation.turn !== null)
|
|
381
|
+
await this.interrupt(conversation, conversation.turn.id);
|
|
204
382
|
}
|
|
205
|
-
|
|
206
|
-
const
|
|
207
|
-
|
|
383
|
+
ahead(conversation, waiting) {
|
|
384
|
+
const behind = conversation.waiting.findIndex((held) => held.call.at_once !== true);
|
|
385
|
+
conversation.waiting.splice(behind === -1 ? conversation.waiting.length : behind, 0, waiting);
|
|
386
|
+
}
|
|
387
|
+
async steer(conversation, running, waiting) {
|
|
388
|
+
let outcome;
|
|
208
389
|
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);
|
|
390
|
+
({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
|
|
391
|
+
sessionId: running.sessionId,
|
|
392
|
+
prompt: waiting.content,
|
|
393
|
+
_meta: { steering: { idleBehavior: 'promptRequired' } },
|
|
394
|
+
}));
|
|
217
395
|
}
|
|
218
396
|
catch (error) {
|
|
219
|
-
|
|
397
|
+
outcome = error;
|
|
398
|
+
}
|
|
399
|
+
if (outcome === 'promptRequired') {
|
|
400
|
+
this.ahead(conversation, waiting);
|
|
401
|
+
}
|
|
402
|
+
else if (outcome === 'injected' || outcome === 'startedNewTurn') {
|
|
403
|
+
this.kit.calls.keepResult(String(waiting.call.input_id), {});
|
|
404
|
+
waiting.settle({});
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
const cause = outcome instanceof Error ? outcome : new Error(`the CLI answered ${JSON.stringify(outcome)}`);
|
|
408
|
+
waiting.settle({ refused: refusal(conversation, new NotStarted(UNSTEERED, { cause })) });
|
|
220
409
|
}
|
|
221
|
-
return {
|
|
222
|
-
ack: new Promise((settle) => {
|
|
223
|
-
conversation.waiting.push({ input, route, content, settle });
|
|
224
|
-
this.next(conversation);
|
|
225
|
-
}),
|
|
226
|
-
};
|
|
227
410
|
}
|
|
228
411
|
next(conversation) {
|
|
229
412
|
if (conversation.sending !== null || conversation.open > 0)
|
|
230
413
|
return;
|
|
231
414
|
const waiting = conversation.waiting.shift();
|
|
232
|
-
if (waiting === undefined)
|
|
415
|
+
if (waiting === undefined) {
|
|
416
|
+
const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
|
|
417
|
+
if (conversation.starting !== step)
|
|
418
|
+
this.starts(conversation, step);
|
|
419
|
+
if (conversation.done && conversation.launch === null)
|
|
420
|
+
void this.stop(conversation).catch(logged);
|
|
233
421
|
return;
|
|
422
|
+
}
|
|
234
423
|
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
235
424
|
conversation.sending = null;
|
|
236
425
|
this.next(conversation);
|
|
237
426
|
});
|
|
238
427
|
}
|
|
239
428
|
async send(conversation, waiting) {
|
|
240
|
-
const {
|
|
241
|
-
let
|
|
429
|
+
const { call } = waiting;
|
|
430
|
+
let result = {};
|
|
431
|
+
let started = false;
|
|
432
|
+
let instructing = waiting.instructing;
|
|
242
433
|
const prompt = [];
|
|
243
434
|
try {
|
|
244
435
|
if (conversation.running === null) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (resumed === null)
|
|
248
|
-
ack = { provider_session_id: opened };
|
|
436
|
+
this.starts(conversation, 'session');
|
|
437
|
+
await this.launched(conversation);
|
|
249
438
|
}
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
439
|
+
if (conversation.running === null) {
|
|
440
|
+
const stored = conversation.session ?? call.provider_session_id;
|
|
441
|
+
const empty = call.first === true && conversation.prompted !== stored;
|
|
442
|
+
const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
|
|
443
|
+
if (call.first === true)
|
|
444
|
+
instructing = this.instructed(conversation, waiting.route);
|
|
445
|
+
started = (await launch.session) !== stored;
|
|
446
|
+
}
|
|
447
|
+
const session = conversation.running.sessionId;
|
|
448
|
+
if (started || (call.first === true && session !== call.provider_session_id)) {
|
|
449
|
+
result = { provider_session_id: session };
|
|
450
|
+
}
|
|
451
|
+
if (call.first === true) {
|
|
452
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
255
453
|
}
|
|
256
454
|
if (conversation.kills > 0)
|
|
257
455
|
throw new Error(KILLED);
|
|
258
456
|
}
|
|
259
457
|
catch (error) {
|
|
260
|
-
|
|
458
|
+
this.starts(conversation, null);
|
|
459
|
+
if (started) {
|
|
261
460
|
conversation.session = null;
|
|
262
461
|
await this.kill(conversation);
|
|
263
462
|
}
|
|
264
|
-
|
|
463
|
+
const refused = refusal(conversation, error);
|
|
464
|
+
const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
|
|
465
|
+
waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
|
|
265
466
|
return;
|
|
266
467
|
}
|
|
267
|
-
this.
|
|
268
|
-
waiting.
|
|
468
|
+
this.starts(conversation, null);
|
|
469
|
+
this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
|
|
470
|
+
waiting.settle(result);
|
|
471
|
+
}
|
|
472
|
+
starts(conversation, step) {
|
|
473
|
+
conversation.starting = step;
|
|
474
|
+
if (conversation.shown !== null)
|
|
475
|
+
clearTimeout(conversation.shown);
|
|
476
|
+
conversation.shown = null;
|
|
477
|
+
if (step === null || conversation.told !== null) {
|
|
478
|
+
this.tell(conversation, step);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
conversation.shown = setTimeout(() => {
|
|
482
|
+
conversation.shown = null;
|
|
483
|
+
if (conversation.starting === step)
|
|
484
|
+
this.tell(conversation, step);
|
|
485
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
269
486
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
487
|
+
prepares(conversation, step) {
|
|
488
|
+
conversation.preparing = step;
|
|
489
|
+
this.next(conversation);
|
|
490
|
+
}
|
|
491
|
+
tell(conversation, step) {
|
|
492
|
+
if (conversation.told === step && !conversation.untold)
|
|
493
|
+
return;
|
|
494
|
+
conversation.told = step;
|
|
495
|
+
this.retell(conversation);
|
|
496
|
+
}
|
|
497
|
+
retell(conversation) {
|
|
498
|
+
conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
|
|
499
|
+
}
|
|
500
|
+
async transcribed(conversation, messageId, text) {
|
|
501
|
+
const paths = linkedPaths(text);
|
|
502
|
+
if (paths.length === 0)
|
|
503
|
+
return [];
|
|
504
|
+
const killed = conversation.killed.signal;
|
|
505
|
+
const interrupted = conversation.interrupted.signal;
|
|
506
|
+
const transcribing = AbortSignal.any([killed, interrupted]);
|
|
507
|
+
let signal = killed;
|
|
508
|
+
const blocks = [];
|
|
509
|
+
try {
|
|
510
|
+
const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
|
|
511
|
+
const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
|
|
512
|
+
for (const path of paths) {
|
|
513
|
+
let answered = await read(path);
|
|
514
|
+
if (answered === null)
|
|
515
|
+
continue;
|
|
516
|
+
signal = transcribing;
|
|
517
|
+
this.prepares(conversation, 'transcription');
|
|
518
|
+
const said = await transcribe(async () => {
|
|
519
|
+
const address = answered ?? (await read(path));
|
|
520
|
+
answered = null;
|
|
521
|
+
if (address === null)
|
|
522
|
+
throw new Error(`${path} is no longer an audio file`);
|
|
523
|
+
return address;
|
|
524
|
+
}, signal);
|
|
525
|
+
const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
|
|
526
|
+
await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
|
|
527
|
+
blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
|
|
285
528
|
}
|
|
286
529
|
}
|
|
287
|
-
|
|
530
|
+
catch (error) {
|
|
531
|
+
if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
|
|
532
|
+
throw new NotStarted(INTERRUPTED, { cause: error });
|
|
533
|
+
throw error;
|
|
534
|
+
}
|
|
535
|
+
return blocks;
|
|
536
|
+
}
|
|
537
|
+
bridged(conversation) {
|
|
538
|
+
if (conversation.bridge === null) {
|
|
539
|
+
const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
540
|
+
conversation.bridge = bridging;
|
|
541
|
+
bridging.catch(() => {
|
|
542
|
+
if (conversation.bridge === bridging)
|
|
543
|
+
conversation.bridge = null;
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
return conversation.bridge;
|
|
547
|
+
}
|
|
548
|
+
closeBridge(conversation, bridging, bridge) {
|
|
549
|
+
bridge.close();
|
|
550
|
+
if (conversation.bridge === bridging)
|
|
551
|
+
conversation.bridge = null;
|
|
552
|
+
}
|
|
553
|
+
closeIdleBridge(conversation) {
|
|
554
|
+
const bridging = conversation.bridge;
|
|
555
|
+
if (bridging === null || conversation.running !== null || conversation.launch !== null)
|
|
556
|
+
return;
|
|
557
|
+
conversation.bridge = null;
|
|
558
|
+
void bridging.then((bridge) => bridge.close(), () => undefined);
|
|
288
559
|
}
|
|
289
560
|
async interrupt(conversation, turnId) {
|
|
290
561
|
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
@@ -295,11 +566,13 @@ export class Conversations {
|
|
|
295
566
|
}
|
|
296
567
|
halt(conversation) {
|
|
297
568
|
if (conversation.opening !== null)
|
|
298
|
-
|
|
569
|
+
killMarked(marker(conversation.opening));
|
|
570
|
+
for (const scope of conversation.leftovers)
|
|
571
|
+
scope.kill();
|
|
299
572
|
if (conversation.running === null)
|
|
300
573
|
return;
|
|
301
574
|
conversation.running.killed = true;
|
|
302
|
-
|
|
575
|
+
conversation.running.scope.kill();
|
|
303
576
|
}
|
|
304
577
|
async kill(conversation) {
|
|
305
578
|
const running = conversation.running;
|
|
@@ -337,53 +610,78 @@ export class Conversations {
|
|
|
337
610
|
}
|
|
338
611
|
return route;
|
|
339
612
|
}
|
|
340
|
-
async start(conversation,
|
|
613
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
614
|
+
const route = await routed;
|
|
341
615
|
const reader = readerFor(route.kind);
|
|
342
616
|
reader.session = sessionId;
|
|
343
617
|
const app = client({ name: '@agentshouse/kit' })
|
|
344
618
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
345
|
-
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
619
|
+
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
620
|
+
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
|
|
346
621
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
347
|
-
const
|
|
622
|
+
const bridging = this.bridged(conversation);
|
|
623
|
+
const bridge = await step(unbridged, bridging);
|
|
348
624
|
if (conversation.kills > 0) {
|
|
349
|
-
|
|
625
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
350
626
|
throw new Error(KILLED);
|
|
351
627
|
}
|
|
352
628
|
conversation.opening = bridge;
|
|
629
|
+
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
630
|
+
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
631
|
+
let adapter = null;
|
|
353
632
|
try {
|
|
354
|
-
|
|
355
|
-
await CLIS[route.kind].allowHouse();
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
633
|
+
bridged(bridge);
|
|
634
|
+
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
635
|
+
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
636
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
637
|
+
work.read(message);
|
|
638
|
+
this.read(conversation, reader, message);
|
|
639
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
640
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
359
641
|
const agent = adapter.connection.agent;
|
|
360
642
|
const cwd = route.working_directory;
|
|
361
|
-
const _meta = CLIS[route.kind].sessionMeta;
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
|
|
643
|
+
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
644
|
+
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
645
|
+
const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
365
646
|
reader.session = opened.sessionId;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
647
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
648
|
+
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
649
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
650
|
+
}
|
|
651
|
+
const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
652
|
+
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
653
|
+
void work.settled().then(() => scope.record());
|
|
369
654
|
conversation.running = running;
|
|
655
|
+
adapter.turn(conversation.open > 0);
|
|
370
656
|
conversation.session = running.sessionId;
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
this.options(conversation, options);
|
|
375
|
-
void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
|
|
657
|
+
conversation.options = options;
|
|
658
|
+
this.facts(conversation);
|
|
659
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
376
660
|
return running.sessionId;
|
|
377
661
|
}
|
|
378
662
|
catch (error) {
|
|
379
|
-
|
|
380
|
-
|
|
663
|
+
adapter?.retire();
|
|
664
|
+
work.close();
|
|
665
|
+
conversation.leftovers.add(scope);
|
|
666
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
381
667
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
382
668
|
}
|
|
383
669
|
finally {
|
|
384
670
|
conversation.opening = null;
|
|
385
671
|
}
|
|
386
672
|
}
|
|
673
|
+
async resume(conversation, kind, adapter, request, empty) {
|
|
674
|
+
try {
|
|
675
|
+
return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
|
|
679
|
+
throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
|
|
680
|
+
}
|
|
681
|
+
logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
387
685
|
async launchSettings(running, route, offered) {
|
|
388
686
|
let options = offered;
|
|
389
687
|
const settings = [
|
|
@@ -405,26 +703,37 @@ export class Conversations {
|
|
|
405
703
|
}
|
|
406
704
|
return options;
|
|
407
705
|
}
|
|
408
|
-
exited(conversation,
|
|
409
|
-
|
|
410
|
-
if (running?.adapter !== adapter)
|
|
411
|
-
return;
|
|
412
|
-
this.detach(conversation);
|
|
706
|
+
exited(conversation, running, cause) {
|
|
707
|
+
this.detach(conversation, running);
|
|
413
708
|
if (conversation.turn !== null) {
|
|
414
|
-
this.end(conversation, conversation.turn,
|
|
709
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
415
710
|
}
|
|
416
711
|
this.next(conversation);
|
|
417
712
|
this.kit.changed();
|
|
418
713
|
}
|
|
419
|
-
detach(conversation) {
|
|
714
|
+
detach(conversation, running) {
|
|
715
|
+
if (conversation.bridge === running.bridging)
|
|
716
|
+
conversation.bridge = null;
|
|
717
|
+
running.work.close();
|
|
718
|
+
conversation.leftovers.add(running.scope);
|
|
420
719
|
conversation.running = null;
|
|
421
720
|
conversation.commands = null;
|
|
422
|
-
conversation.
|
|
721
|
+
conversation.options = null;
|
|
423
722
|
for (const [id, question] of this.questions) {
|
|
424
723
|
if (question.conversation === conversation)
|
|
425
724
|
this.questions.delete(id);
|
|
426
725
|
}
|
|
427
|
-
this.
|
|
726
|
+
this.process(conversation, false);
|
|
727
|
+
}
|
|
728
|
+
process(conversation, running) {
|
|
729
|
+
conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
|
|
730
|
+
}
|
|
731
|
+
facts(conversation) {
|
|
732
|
+
this.process(conversation, true);
|
|
733
|
+
if (conversation.commands !== null)
|
|
734
|
+
this.commands(conversation, conversation.commands);
|
|
735
|
+
if (conversation.options !== null)
|
|
736
|
+
this.options(conversation, conversation.options);
|
|
428
737
|
}
|
|
429
738
|
commands(conversation, commands) {
|
|
430
739
|
conversation.commands = commands;
|
|
@@ -435,6 +744,7 @@ export class Conversations {
|
|
|
435
744
|
options(conversation, options) {
|
|
436
745
|
if (conversation.running === null)
|
|
437
746
|
return;
|
|
747
|
+
conversation.options = options;
|
|
438
748
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
439
749
|
}
|
|
440
750
|
deliver(conversation, turn, event, body) {
|
|
@@ -449,7 +759,7 @@ export class Conversations {
|
|
|
449
759
|
return delivered;
|
|
450
760
|
}
|
|
451
761
|
write(conversation, turn, operation) {
|
|
452
|
-
if (
|
|
762
|
+
if (turn.ended || !this.watched)
|
|
453
763
|
return;
|
|
454
764
|
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
455
765
|
}
|
|
@@ -460,61 +770,74 @@ export class Conversations {
|
|
|
460
770
|
begin(conversation) {
|
|
461
771
|
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
462
772
|
conversation.open++;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
turn.admitted = true;
|
|
466
|
-
this.replay(conversation, turn);
|
|
467
|
-
})
|
|
468
|
-
.catch(logged);
|
|
773
|
+
conversation.running?.adapter.turn(true);
|
|
774
|
+
void this.deliver(conversation, turn, 'started', {}).catch(logged);
|
|
469
775
|
conversation.turn = turn;
|
|
470
776
|
if (conversation.context !== null)
|
|
471
777
|
turn.parts.measure(conversation.context);
|
|
472
778
|
for (const part of conversation.held.splice(0))
|
|
473
779
|
turn.parts.start(part);
|
|
474
|
-
for (const event of conversation.running?.reader.turnStarted() ?? [])
|
|
475
|
-
this.event(conversation, event);
|
|
476
780
|
this.kit.changed();
|
|
477
781
|
return turn;
|
|
478
782
|
}
|
|
479
|
-
end(conversation, turn, failed) {
|
|
783
|
+
end(conversation, turn, failed, notice = null) {
|
|
480
784
|
if (turn.ended)
|
|
481
785
|
return;
|
|
786
|
+
if (failed !== null)
|
|
787
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
482
788
|
if (conversation.turn === turn) {
|
|
483
789
|
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
484
790
|
this.apply(conversation, turn, event);
|
|
485
791
|
}
|
|
792
|
+
if (notice !== null)
|
|
793
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
486
794
|
turn.ended = true;
|
|
487
795
|
conversation.open--;
|
|
796
|
+
conversation.running?.adapter.turn(conversation.open > 0);
|
|
488
797
|
if (conversation.turn === turn)
|
|
489
798
|
conversation.turn = null;
|
|
490
799
|
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
491
|
-
parts: turn.parts.
|
|
800
|
+
parts: turn.parts.saved(),
|
|
492
801
|
context: turn.parts.context,
|
|
493
|
-
...failed,
|
|
802
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
494
803
|
}));
|
|
495
804
|
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
496
805
|
this.next(conversation);
|
|
497
806
|
this.kit.changed();
|
|
498
807
|
}
|
|
499
|
-
prompt(conversation, prompt) {
|
|
808
|
+
prompt(conversation, prompt, taken) {
|
|
500
809
|
const running = conversation.running;
|
|
501
810
|
const turn = this.begin(conversation);
|
|
502
811
|
turn.prompted = true;
|
|
503
|
-
|
|
812
|
+
turn.taken = taken;
|
|
813
|
+
conversation.prompted = running.sessionId;
|
|
814
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
815
|
+
this.took(turn);
|
|
816
|
+
const notice = answeredFailure(answer);
|
|
817
|
+
if (notice !== null) {
|
|
818
|
+
this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
822
|
+
}
|
|
823
|
+
}, async (error) => {
|
|
504
824
|
if (running.adapter.connection.signal.aborted) {
|
|
505
|
-
this.end(conversation, turn,
|
|
825
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
506
826
|
}
|
|
507
827
|
else {
|
|
828
|
+
this.took(turn);
|
|
508
829
|
if (conversation.open === 1 && conversation.running === running) {
|
|
509
|
-
|
|
510
|
-
this.detach(conversation);
|
|
830
|
+
running.adapter.retire();
|
|
831
|
+
this.detach(conversation, running);
|
|
511
832
|
}
|
|
512
|
-
this.end(conversation, turn,
|
|
833
|
+
this.end(conversation, turn, answeredWith(error));
|
|
513
834
|
}
|
|
514
835
|
});
|
|
515
836
|
}
|
|
516
837
|
started(conversation) {
|
|
517
|
-
|
|
838
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
839
|
+
this.took(turn);
|
|
840
|
+
return turn;
|
|
518
841
|
}
|
|
519
842
|
ended(conversation) {
|
|
520
843
|
const turn = conversation.turn;
|
|
@@ -592,20 +915,6 @@ export class Conversations {
|
|
|
592
915
|
case 'options':
|
|
593
916
|
this.options(conversation, event.options);
|
|
594
917
|
return;
|
|
595
|
-
case 'busy':
|
|
596
|
-
if (event.running)
|
|
597
|
-
conversation.busy.add(event.key);
|
|
598
|
-
else
|
|
599
|
-
conversation.busy.delete(event.key);
|
|
600
|
-
this.kit.changed();
|
|
601
|
-
return;
|
|
602
|
-
case 'servers': {
|
|
603
|
-
const running = conversation.running;
|
|
604
|
-
if (running?.baseline)
|
|
605
|
-
for (const found of processes(marker(running.bridge)))
|
|
606
|
-
running.baseline.add(found);
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
918
|
case 'context':
|
|
610
919
|
conversation.context = { used: event.used, window: event.window };
|
|
611
920
|
conversation.turn?.parts.measure(conversation.context);
|
|
@@ -623,6 +932,11 @@ export class Conversations {
|
|
|
623
932
|
}
|
|
624
933
|
this.apply(conversation, this.started(conversation), event);
|
|
625
934
|
}
|
|
935
|
+
took(turn) {
|
|
936
|
+
const taken = turn.taken;
|
|
937
|
+
turn.taken = null;
|
|
938
|
+
taken?.();
|
|
939
|
+
}
|
|
626
940
|
apply(conversation, turn, event) {
|
|
627
941
|
switch (event.kind) {
|
|
628
942
|
case 'text':
|
|
@@ -647,7 +961,7 @@ export class Conversations {
|
|
|
647
961
|
turn.parts.plan(event.entries);
|
|
648
962
|
return;
|
|
649
963
|
case 'image':
|
|
650
|
-
turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
|
|
964
|
+
turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
|
|
651
965
|
this.kit.changed();
|
|
652
966
|
return;
|
|
653
967
|
}
|
|
@@ -667,23 +981,30 @@ export class Conversations {
|
|
|
667
981
|
else
|
|
668
982
|
conversation.turn.parts.start(part);
|
|
669
983
|
}
|
|
670
|
-
async image(conversation, turn, data, mediaType) {
|
|
984
|
+
async image(conversation, turn, reserved, data, mediaType) {
|
|
671
985
|
const bytes = Buffer.from(data, 'base64');
|
|
672
986
|
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
987
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
673
988
|
try {
|
|
674
989
|
const declared = (await this.deliver(conversation, turn, 'images', {
|
|
675
990
|
name,
|
|
676
991
|
media_type: mediaType,
|
|
677
992
|
bytes: bytes.length,
|
|
678
|
-
sha256
|
|
993
|
+
sha256,
|
|
679
994
|
}));
|
|
680
|
-
const saved =
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
995
|
+
const saved = await uploaded(declared.upload, {
|
|
996
|
+
name,
|
|
997
|
+
bytes: bytes.length,
|
|
998
|
+
sha256,
|
|
999
|
+
read: (start, end) => [bytes.subarray(start, end)],
|
|
1000
|
+
send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
|
|
1001
|
+
finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
|
|
1002
|
+
});
|
|
1003
|
+
turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
|
|
684
1004
|
}
|
|
685
1005
|
catch (error) {
|
|
686
|
-
|
|
1006
|
+
const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
1007
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
|
|
687
1008
|
}
|
|
688
1009
|
}
|
|
689
1010
|
}
|