@agentshouse/kit 0.2.1-cca496635583e → 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 +36 -35
- package/dist/acp.js +85 -18
- package/dist/agents.js +110 -23
- package/dist/api.js +5 -1
- package/dist/app-git.js +10 -26
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +4 -7
- package/dist/building.js +46 -0
- package/dist/calls.js +71 -0
- package/dist/chatgpt.js +32 -0
- package/dist/clis-main.js +2 -2
- package/dist/clis.js +87 -22
- package/dist/connection.js +116 -0
- package/dist/conversations.js +480 -159
- package/dist/copies.js +89 -52
- package/dist/end.js +11 -2
- package/dist/events.js +6 -2
- package/dist/files.js +139 -31
- package/dist/git.js +13 -11
- package/dist/home.js +56 -5
- package/dist/house-cli.js +299 -0
- package/dist/house-main.js +7 -189
- package/dist/instructions.js +7 -11
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -22
- package/dist/login.js +50 -16
- package/dist/logout.js +0 -2
- package/dist/mcp.js +6 -2
- package/dist/parts.js +34 -16
- package/dist/placement.js +39 -0
- package/dist/relay.js +22 -1
- package/dist/resident.js +94 -34
- package/dist/scope.js +3 -0
- package/dist/scripts.js +255 -0
- package/dist/service.js +23 -1
- package/dist/sign-in.js +22 -39
- package/dist/silence.js +28 -0
- package/dist/skills.js +49 -35
- package/dist/transcription.js +85 -45
- package/dist/translate.js +2 -8
- package/dist/update.js +115 -53
- package/dist/wire.js +22 -0
- package/dist/work.js +3 -5
- package/package.json +20 -20
- package/dist/bootstrap.json +0 -24
- package/dist/stream.js +0 -74
- 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,41 +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
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';
|
|
14
17
|
import { killMarked, Scope } from './scope.js';
|
|
15
18
|
import { holdSecretInput } from './secret-input.js';
|
|
16
|
-
import { createHowWeWork } from './skills.js';
|
|
17
19
|
import { transcribe } from './transcription.js';
|
|
18
20
|
import { workFor } from './work.js';
|
|
21
|
+
const SIGNED_OUT = RequestError.authRequired().code;
|
|
19
22
|
const KILLED = 'the conversation was killed';
|
|
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
|
}
|
|
109
|
+
function steers(adapter) {
|
|
110
|
+
return adapter.initialized._meta?.steering?.supported === true;
|
|
111
|
+
}
|
|
32
112
|
class Turn {
|
|
33
113
|
id = randomUUID();
|
|
34
114
|
parts;
|
|
35
|
-
admitted = false;
|
|
36
115
|
questions = 0;
|
|
37
116
|
secrets = 0;
|
|
38
117
|
prompted = false;
|
|
118
|
+
taken = null;
|
|
39
119
|
ended = false;
|
|
40
120
|
images = 0;
|
|
41
121
|
uploads = [];
|
|
@@ -49,13 +129,19 @@ class Conversation {
|
|
|
49
129
|
session = null;
|
|
50
130
|
turn = null;
|
|
51
131
|
commands = null;
|
|
132
|
+
options = null;
|
|
133
|
+
exitUntold = false;
|
|
52
134
|
context = null;
|
|
53
135
|
leftovers = new Set();
|
|
54
136
|
waiting = [];
|
|
137
|
+
done = false;
|
|
55
138
|
sending = null;
|
|
56
139
|
subagents = new Map();
|
|
57
140
|
held = [];
|
|
58
141
|
opening = null;
|
|
142
|
+
bridge = null;
|
|
143
|
+
launch = null;
|
|
144
|
+
prompted = null;
|
|
59
145
|
kills = 0;
|
|
60
146
|
killed = new AbortController();
|
|
61
147
|
interrupts = 0;
|
|
@@ -63,6 +149,11 @@ class Conversation {
|
|
|
63
149
|
open = 0;
|
|
64
150
|
queue = Promise.resolve();
|
|
65
151
|
reports = Promise.resolve();
|
|
152
|
+
starting = null;
|
|
153
|
+
preparing = null;
|
|
154
|
+
told = null;
|
|
155
|
+
untold = false;
|
|
156
|
+
shown = null;
|
|
66
157
|
constructor(id) {
|
|
67
158
|
this.id = id;
|
|
68
159
|
}
|
|
@@ -78,9 +169,7 @@ function asksSecret(request) {
|
|
|
78
169
|
export class Conversations {
|
|
79
170
|
kit;
|
|
80
171
|
conversations = new Map();
|
|
81
|
-
acks = new Map();
|
|
82
172
|
questions = new Map();
|
|
83
|
-
carrying = 0;
|
|
84
173
|
reporting = 0;
|
|
85
174
|
watched = false;
|
|
86
175
|
constructor(kit) {
|
|
@@ -88,38 +177,27 @@ export class Conversations {
|
|
|
88
177
|
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
89
178
|
setInterval(() => this.look(), 2_000);
|
|
90
179
|
}
|
|
91
|
-
|
|
92
|
-
this.
|
|
93
|
-
|
|
94
|
-
(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
conversation.queue = handled.catch(() => undefined);
|
|
109
|
-
const work = handled.then(({ ack }) => ack);
|
|
110
|
-
this.acks.set(input.input_id, work);
|
|
111
|
-
return work;
|
|
112
|
-
})();
|
|
113
|
-
carried
|
|
114
|
-
.then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
|
|
115
|
-
.catch(logged)
|
|
116
|
-
.finally(() => {
|
|
117
|
-
this.carrying--;
|
|
118
|
-
this.kit.changed();
|
|
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 }));
|
|
119
197
|
});
|
|
120
198
|
}
|
|
121
199
|
idle() {
|
|
122
|
-
if (this.
|
|
200
|
+
if (this.reporting > 0)
|
|
123
201
|
return false;
|
|
124
202
|
for (const conversation of this.conversations.values()) {
|
|
125
203
|
const turn = conversation.turn;
|
|
@@ -148,6 +226,14 @@ export class Conversations {
|
|
|
148
226
|
}
|
|
149
227
|
opened() {
|
|
150
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
|
+
}
|
|
151
237
|
}
|
|
152
238
|
watching(watched) {
|
|
153
239
|
const resend = watched && !this.watched;
|
|
@@ -167,127 +253,309 @@ export class Conversations {
|
|
|
167
253
|
}
|
|
168
254
|
return conversation;
|
|
169
255
|
}
|
|
170
|
-
async carry(conversation,
|
|
171
|
-
if (
|
|
172
|
-
return
|
|
173
|
-
if (
|
|
174
|
-
return this.message(conversation,
|
|
175
|
-
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') {
|
|
176
268
|
if (--conversation.interrupts === 0)
|
|
177
269
|
conversation.interrupted = new AbortController();
|
|
178
|
-
await this.interrupt(conversation, String(
|
|
270
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
179
271
|
}
|
|
180
|
-
if (
|
|
272
|
+
if (call.kind === 'kill') {
|
|
181
273
|
await this.kill(conversation);
|
|
182
274
|
await conversation.sending;
|
|
183
275
|
if (--conversation.kills === 0)
|
|
184
276
|
conversation.killed = new AbortController();
|
|
185
277
|
}
|
|
186
|
-
if (
|
|
187
|
-
await this.option(conversation, String(
|
|
188
|
-
if (
|
|
189
|
-
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));
|
|
190
282
|
if (question === undefined) {
|
|
191
|
-
const text = answerMessage(
|
|
192
|
-
return this.message(conversation, { ...
|
|
283
|
+
const text = answerMessage(call.request, call.response);
|
|
284
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
193
285
|
}
|
|
194
|
-
question.answer(
|
|
286
|
+
question.answer(call.response);
|
|
195
287
|
}
|
|
196
|
-
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
|
+
};
|
|
197
296
|
}
|
|
198
|
-
async
|
|
199
|
-
|
|
200
|
-
|
|
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;
|
|
201
345
|
try {
|
|
202
|
-
|
|
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);
|
|
203
353
|
}
|
|
204
354
|
catch (error) {
|
|
205
|
-
|
|
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;
|
|
206
378
|
}
|
|
379
|
+
this.ahead(conversation, waiting);
|
|
380
|
+
if (conversation.turn !== null)
|
|
381
|
+
await this.interrupt(conversation, conversation.turn.id);
|
|
207
382
|
}
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
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;
|
|
211
389
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
content.push({ type: 'text', text: lines.join('\n') });
|
|
218
|
-
if (conversation.kills > 0)
|
|
219
|
-
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
|
+
}));
|
|
220
395
|
}
|
|
221
396
|
catch (error) {
|
|
222
|
-
|
|
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 })) });
|
|
223
409
|
}
|
|
224
|
-
return {
|
|
225
|
-
ack: new Promise((settle) => {
|
|
226
|
-
conversation.waiting.push({ input, route, content, settle });
|
|
227
|
-
this.next(conversation);
|
|
228
|
-
}),
|
|
229
|
-
};
|
|
230
410
|
}
|
|
231
411
|
next(conversation) {
|
|
232
412
|
if (conversation.sending !== null || conversation.open > 0)
|
|
233
413
|
return;
|
|
234
414
|
const waiting = conversation.waiting.shift();
|
|
235
|
-
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);
|
|
236
421
|
return;
|
|
422
|
+
}
|
|
237
423
|
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
238
424
|
conversation.sending = null;
|
|
239
425
|
this.next(conversation);
|
|
240
426
|
});
|
|
241
427
|
}
|
|
242
428
|
async send(conversation, waiting) {
|
|
243
|
-
const {
|
|
244
|
-
let
|
|
429
|
+
const { call } = waiting;
|
|
430
|
+
let result = {};
|
|
431
|
+
let started = false;
|
|
432
|
+
let instructing = waiting.instructing;
|
|
245
433
|
const prompt = [];
|
|
246
434
|
try {
|
|
247
435
|
if (conversation.running === null) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (resumed === null)
|
|
251
|
-
ack = { provider_session_id: opened };
|
|
436
|
+
this.starts(conversation, 'session');
|
|
437
|
+
await this.launched(conversation);
|
|
252
438
|
}
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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)) });
|
|
258
453
|
}
|
|
259
454
|
if (conversation.kills > 0)
|
|
260
455
|
throw new Error(KILLED);
|
|
261
456
|
}
|
|
262
457
|
catch (error) {
|
|
263
|
-
|
|
458
|
+
this.starts(conversation, null);
|
|
459
|
+
if (started) {
|
|
264
460
|
conversation.session = null;
|
|
265
461
|
await this.kill(conversation);
|
|
266
462
|
}
|
|
267
|
-
|
|
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 });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
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);
|
|
268
479
|
return;
|
|
269
480
|
}
|
|
270
|
-
|
|
271
|
-
|
|
481
|
+
conversation.shown = setTimeout(() => {
|
|
482
|
+
conversation.shown = null;
|
|
483
|
+
if (conversation.starting === step)
|
|
484
|
+
this.tell(conversation, step);
|
|
485
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
272
486
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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}` });
|
|
288
528
|
}
|
|
289
529
|
}
|
|
290
|
-
|
|
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);
|
|
291
559
|
}
|
|
292
560
|
async interrupt(conversation, turnId) {
|
|
293
561
|
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
@@ -342,63 +610,78 @@ export class Conversations {
|
|
|
342
610
|
}
|
|
343
611
|
return route;
|
|
344
612
|
}
|
|
345
|
-
async start(conversation,
|
|
613
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
614
|
+
const route = await routed;
|
|
346
615
|
const reader = readerFor(route.kind);
|
|
347
616
|
reader.session = sessionId;
|
|
348
617
|
const app = client({ name: '@agentshouse/kit' })
|
|
349
618
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
350
|
-
.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));
|
|
351
621
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
352
|
-
const
|
|
622
|
+
const bridging = this.bridged(conversation);
|
|
623
|
+
const bridge = await step(unbridged, bridging);
|
|
353
624
|
if (conversation.kills > 0) {
|
|
354
|
-
|
|
625
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
355
626
|
throw new Error(KILLED);
|
|
356
627
|
}
|
|
357
628
|
conversation.opening = bridge;
|
|
358
629
|
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
359
630
|
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
631
|
+
let adapter = null;
|
|
360
632
|
try {
|
|
361
|
-
|
|
633
|
+
bridged(bridge);
|
|
362
634
|
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
363
|
-
const
|
|
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) => {
|
|
364
637
|
work.read(message);
|
|
365
638
|
this.read(conversation, reader, message);
|
|
366
|
-
}, { ...bridge.env, ...
|
|
367
|
-
void adapter.exited.then(() =>
|
|
639
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
640
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
368
641
|
const agent = adapter.connection.agent;
|
|
369
642
|
const cwd = route.working_directory;
|
|
370
643
|
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
|
|
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 })));
|
|
374
646
|
reader.session = opened.sessionId;
|
|
375
|
-
if (sessionId
|
|
647
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
376
648
|
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
377
|
-
await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
|
|
649
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
378
650
|
}
|
|
379
|
-
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
380
|
-
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
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 ?? []));
|
|
381
653
|
void work.settled().then(() => scope.record());
|
|
382
654
|
conversation.running = running;
|
|
655
|
+
adapter.turn(conversation.open > 0);
|
|
383
656
|
conversation.session = running.sessionId;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
this.options(conversation, options);
|
|
388
|
-
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));
|
|
389
660
|
return running.sessionId;
|
|
390
661
|
}
|
|
391
662
|
catch (error) {
|
|
392
|
-
|
|
663
|
+
adapter?.retire();
|
|
393
664
|
work.close();
|
|
394
665
|
conversation.leftovers.add(scope);
|
|
395
|
-
|
|
666
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
396
667
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
397
668
|
}
|
|
398
669
|
finally {
|
|
399
670
|
conversation.opening = null;
|
|
400
671
|
}
|
|
401
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
|
+
}
|
|
402
685
|
async launchSettings(running, route, offered) {
|
|
403
686
|
let options = offered;
|
|
404
687
|
const settings = [
|
|
@@ -420,27 +703,37 @@ export class Conversations {
|
|
|
420
703
|
}
|
|
421
704
|
return options;
|
|
422
705
|
}
|
|
423
|
-
exited(conversation,
|
|
424
|
-
const running = conversation.running;
|
|
425
|
-
if (running?.adapter !== adapter)
|
|
426
|
-
return;
|
|
706
|
+
exited(conversation, running, cause) {
|
|
427
707
|
this.detach(conversation, running);
|
|
428
708
|
if (conversation.turn !== null) {
|
|
429
|
-
this.end(conversation, conversation.turn,
|
|
709
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
430
710
|
}
|
|
431
711
|
this.next(conversation);
|
|
432
712
|
this.kit.changed();
|
|
433
713
|
}
|
|
434
714
|
detach(conversation, running) {
|
|
715
|
+
if (conversation.bridge === running.bridging)
|
|
716
|
+
conversation.bridge = null;
|
|
435
717
|
running.work.close();
|
|
436
718
|
conversation.leftovers.add(running.scope);
|
|
437
719
|
conversation.running = null;
|
|
438
720
|
conversation.commands = null;
|
|
721
|
+
conversation.options = null;
|
|
439
722
|
for (const [id, question] of this.questions) {
|
|
440
723
|
if (question.conversation === conversation)
|
|
441
724
|
this.questions.delete(id);
|
|
442
725
|
}
|
|
443
|
-
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);
|
|
444
737
|
}
|
|
445
738
|
commands(conversation, commands) {
|
|
446
739
|
conversation.commands = commands;
|
|
@@ -451,6 +744,7 @@ export class Conversations {
|
|
|
451
744
|
options(conversation, options) {
|
|
452
745
|
if (conversation.running === null)
|
|
453
746
|
return;
|
|
747
|
+
conversation.options = options;
|
|
454
748
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
455
749
|
}
|
|
456
750
|
deliver(conversation, turn, event, body) {
|
|
@@ -465,7 +759,7 @@ export class Conversations {
|
|
|
465
759
|
return delivered;
|
|
466
760
|
}
|
|
467
761
|
write(conversation, turn, operation) {
|
|
468
|
-
if (
|
|
762
|
+
if (turn.ended || !this.watched)
|
|
469
763
|
return;
|
|
470
764
|
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
471
765
|
}
|
|
@@ -476,12 +770,8 @@ export class Conversations {
|
|
|
476
770
|
begin(conversation) {
|
|
477
771
|
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
478
772
|
conversation.open++;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
turn.admitted = true;
|
|
482
|
-
this.replay(conversation, turn);
|
|
483
|
-
})
|
|
484
|
-
.catch(logged);
|
|
773
|
+
conversation.running?.adapter.turn(true);
|
|
774
|
+
void this.deliver(conversation, turn, 'started', {}).catch(logged);
|
|
485
775
|
conversation.turn = turn;
|
|
486
776
|
if (conversation.context !== null)
|
|
487
777
|
turn.parts.measure(conversation.context);
|
|
@@ -490,45 +780,64 @@ export class Conversations {
|
|
|
490
780
|
this.kit.changed();
|
|
491
781
|
return turn;
|
|
492
782
|
}
|
|
493
|
-
end(conversation, turn, failed) {
|
|
783
|
+
end(conversation, turn, failed, notice = null) {
|
|
494
784
|
if (turn.ended)
|
|
495
785
|
return;
|
|
786
|
+
if (failed !== null)
|
|
787
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
496
788
|
if (conversation.turn === turn) {
|
|
497
789
|
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
498
790
|
this.apply(conversation, turn, event);
|
|
499
791
|
}
|
|
792
|
+
if (notice !== null)
|
|
793
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
500
794
|
turn.ended = true;
|
|
501
795
|
conversation.open--;
|
|
796
|
+
conversation.running?.adapter.turn(conversation.open > 0);
|
|
502
797
|
if (conversation.turn === turn)
|
|
503
798
|
conversation.turn = null;
|
|
504
799
|
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
505
|
-
parts: turn.parts.
|
|
800
|
+
parts: turn.parts.saved(),
|
|
506
801
|
context: turn.parts.context,
|
|
507
|
-
...failed,
|
|
802
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
508
803
|
}));
|
|
509
804
|
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
510
805
|
this.next(conversation);
|
|
511
806
|
this.kit.changed();
|
|
512
807
|
}
|
|
513
|
-
prompt(conversation, prompt) {
|
|
808
|
+
prompt(conversation, prompt, taken) {
|
|
514
809
|
const running = conversation.running;
|
|
515
810
|
const turn = this.begin(conversation);
|
|
516
811
|
turn.prompted = true;
|
|
517
|
-
|
|
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) => {
|
|
518
824
|
if (running.adapter.connection.signal.aborted) {
|
|
519
|
-
this.end(conversation, turn,
|
|
825
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
520
826
|
}
|
|
521
827
|
else {
|
|
828
|
+
this.took(turn);
|
|
522
829
|
if (conversation.open === 1 && conversation.running === running) {
|
|
523
|
-
running.
|
|
830
|
+
running.adapter.retire();
|
|
524
831
|
this.detach(conversation, running);
|
|
525
832
|
}
|
|
526
|
-
this.end(conversation, turn,
|
|
833
|
+
this.end(conversation, turn, answeredWith(error));
|
|
527
834
|
}
|
|
528
835
|
});
|
|
529
836
|
}
|
|
530
837
|
started(conversation) {
|
|
531
|
-
|
|
838
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
839
|
+
this.took(turn);
|
|
840
|
+
return turn;
|
|
532
841
|
}
|
|
533
842
|
ended(conversation) {
|
|
534
843
|
const turn = conversation.turn;
|
|
@@ -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
|
}
|