@agentshouse/kit 0.2.1-c3cf4b145dcda → 0.2.1-c3d0b9ac4a82a
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 +15 -44
- package/dist/building.js +46 -0
- package/dist/calls.js +63 -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 +456 -200
- 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 -162
- 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 +116 -51
- package/dist/scope.js +206 -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/web-wizard.js +101 -0
- 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,117 @@
|
|
|
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
|
+
class NotStarted extends Error {
|
|
38
|
+
}
|
|
39
|
+
class ResumeRefused extends NotStarted {
|
|
40
|
+
}
|
|
41
|
+
async function step(reason, work) {
|
|
42
|
+
try {
|
|
43
|
+
return await work;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new NotStarted(typeof reason === 'string' ? reason : reason(error), { cause: error });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function signedOut(error) {
|
|
50
|
+
return typeof error === 'object' && error !== null && error.code === SIGNED_OUT;
|
|
51
|
+
}
|
|
52
|
+
function unopened(reason) {
|
|
53
|
+
return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
|
|
54
|
+
}
|
|
55
|
+
// A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
|
|
56
|
+
const STARTING_SHOWN_AFTER_MS = 1_000;
|
|
57
|
+
function unbridged(error) {
|
|
58
|
+
return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
|
|
59
|
+
}
|
|
60
|
+
const EXIT_PLAN_MODE = {
|
|
61
|
+
parse(value) {
|
|
62
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
63
|
+
throw new Error('_x.ai/exit_plan_mode needs an object');
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'option', 'answer']);
|
|
20
69
|
function causeOf(error) {
|
|
21
70
|
return error instanceof Error ? error.message : String(error);
|
|
22
71
|
}
|
|
72
|
+
function answeredWith(error) {
|
|
73
|
+
return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
74
|
+
}
|
|
75
|
+
function exitedWith(running, cause) {
|
|
76
|
+
if (running.killed)
|
|
77
|
+
return { failure: 'killed', cause: KILLED };
|
|
78
|
+
return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
|
|
79
|
+
}
|
|
23
80
|
function answerMessage(request, response) {
|
|
24
81
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
25
82
|
}
|
|
83
|
+
async function sentPart(url, bytes, headers) {
|
|
84
|
+
const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
|
|
85
|
+
await answer.arrayBuffer();
|
|
86
|
+
if (!answer.ok)
|
|
87
|
+
throw storeRefusal('upload', answer.status);
|
|
88
|
+
}
|
|
26
89
|
function logged(error) {
|
|
27
90
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
28
91
|
}
|
|
92
|
+
function refusal(conversation, error) {
|
|
93
|
+
if (conversation.kills > 0)
|
|
94
|
+
return KILLED;
|
|
95
|
+
const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
|
|
96
|
+
logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
|
|
97
|
+
return causeOf(error);
|
|
98
|
+
}
|
|
99
|
+
function audioOf(result) {
|
|
100
|
+
if (result.isError === true)
|
|
101
|
+
return null;
|
|
102
|
+
const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
|
|
103
|
+
return file?.media_type?.startsWith('audio/') === true ? file : null;
|
|
104
|
+
}
|
|
29
105
|
function marker(bridge) {
|
|
30
106
|
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
31
107
|
}
|
|
32
|
-
function stop(bridge) {
|
|
33
|
-
killMarked(marker(bridge));
|
|
34
|
-
}
|
|
35
108
|
class Turn {
|
|
36
109
|
id = randomUUID();
|
|
37
110
|
parts;
|
|
38
|
-
admitted = false;
|
|
39
111
|
questions = 0;
|
|
40
112
|
secrets = 0;
|
|
41
113
|
prompted = false;
|
|
114
|
+
taken = null;
|
|
42
115
|
ended = false;
|
|
43
116
|
images = 0;
|
|
44
117
|
uploads = [];
|
|
@@ -52,13 +125,18 @@ class Conversation {
|
|
|
52
125
|
session = null;
|
|
53
126
|
turn = null;
|
|
54
127
|
commands = null;
|
|
128
|
+
options = null;
|
|
129
|
+
exitUntold = false;
|
|
55
130
|
context = null;
|
|
56
|
-
|
|
131
|
+
leftovers = new Set();
|
|
57
132
|
waiting = [];
|
|
58
133
|
sending = null;
|
|
59
134
|
subagents = new Map();
|
|
60
135
|
held = [];
|
|
61
136
|
opening = null;
|
|
137
|
+
bridge = null;
|
|
138
|
+
launch = null;
|
|
139
|
+
prompted = null;
|
|
62
140
|
kills = 0;
|
|
63
141
|
killed = new AbortController();
|
|
64
142
|
interrupts = 0;
|
|
@@ -66,6 +144,11 @@ class Conversation {
|
|
|
66
144
|
open = 0;
|
|
67
145
|
queue = Promise.resolve();
|
|
68
146
|
reports = Promise.resolve();
|
|
147
|
+
starting = null;
|
|
148
|
+
preparing = null;
|
|
149
|
+
told = null;
|
|
150
|
+
untold = false;
|
|
151
|
+
shown = null;
|
|
69
152
|
constructor(id) {
|
|
70
153
|
this.id = id;
|
|
71
154
|
}
|
|
@@ -81,70 +164,71 @@ function asksSecret(request) {
|
|
|
81
164
|
export class Conversations {
|
|
82
165
|
kit;
|
|
83
166
|
conversations = new Map();
|
|
84
|
-
acks = new Map();
|
|
85
167
|
questions = new Map();
|
|
86
|
-
carrying = 0;
|
|
87
168
|
reporting = 0;
|
|
88
169
|
watched = false;
|
|
89
|
-
recheck = null;
|
|
90
170
|
constructor(kit) {
|
|
91
171
|
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();
|
|
172
|
+
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
173
|
+
setInterval(() => this.look(), 2_000);
|
|
174
|
+
}
|
|
175
|
+
call(call) {
|
|
176
|
+
void this.kit.calls.carry(call.input_id, () => {
|
|
177
|
+
const conversation = this.conversation(call.conversation_id);
|
|
178
|
+
if (call.kind === 'kill') {
|
|
179
|
+
conversation.kills++;
|
|
180
|
+
conversation.killed.abort(new Error(KILLED));
|
|
181
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
182
|
+
waiting.settle({ refused: KILLED });
|
|
183
|
+
this.halt(conversation);
|
|
184
|
+
}
|
|
185
|
+
if (call.kind === 'interrupt') {
|
|
186
|
+
conversation.interrupts++;
|
|
187
|
+
conversation.interrupted.abort();
|
|
188
|
+
}
|
|
189
|
+
const handled = conversation.queue.then(() => this.carry(conversation, call));
|
|
190
|
+
conversation.queue = handled.catch(() => undefined);
|
|
191
|
+
return handled.then(async ({ result }) => ({ result: await result }));
|
|
121
192
|
});
|
|
122
193
|
}
|
|
123
194
|
idle() {
|
|
124
|
-
if (this.
|
|
195
|
+
if (this.reporting > 0)
|
|
125
196
|
return false;
|
|
126
197
|
for (const conversation of this.conversations.values()) {
|
|
127
198
|
const turn = conversation.turn;
|
|
128
199
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
129
|
-
if (conversation.
|
|
200
|
+
if (conversation.open > (asking ? 1 : 0))
|
|
130
201
|
return false;
|
|
131
202
|
}
|
|
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);
|
|
203
|
+
const conversations = [...this.conversations.values()];
|
|
204
|
+
if (conversations.some((conversation) => conversation.running?.work.busy()))
|
|
142
205
|
return false;
|
|
206
|
+
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
207
|
+
}
|
|
208
|
+
quiet() {
|
|
209
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.open === 0);
|
|
210
|
+
}
|
|
211
|
+
look() {
|
|
212
|
+
for (const conversation of this.conversations.values()) {
|
|
213
|
+
for (const scope of conversation.leftovers) {
|
|
214
|
+
if (!scope.empty())
|
|
215
|
+
continue;
|
|
216
|
+
scope.close();
|
|
217
|
+
conversation.leftovers.delete(scope);
|
|
218
|
+
}
|
|
143
219
|
}
|
|
144
|
-
|
|
220
|
+
this.kit.changed();
|
|
145
221
|
}
|
|
146
222
|
opened() {
|
|
147
223
|
this.watched = false;
|
|
224
|
+
for (const conversation of this.conversations.values()) {
|
|
225
|
+
if (conversation.running !== null)
|
|
226
|
+
this.facts(conversation);
|
|
227
|
+
else if (conversation.exitUntold)
|
|
228
|
+
this.process(conversation, false);
|
|
229
|
+
if (conversation.untold || conversation.told !== null)
|
|
230
|
+
this.retell(conversation);
|
|
231
|
+
}
|
|
148
232
|
}
|
|
149
233
|
watching(watched) {
|
|
150
234
|
const resend = watched && !this.watched;
|
|
@@ -164,63 +248,99 @@ export class Conversations {
|
|
|
164
248
|
}
|
|
165
249
|
return conversation;
|
|
166
250
|
}
|
|
167
|
-
async carry(conversation,
|
|
168
|
-
if (
|
|
169
|
-
return {
|
|
170
|
-
if (
|
|
171
|
-
return this.message(conversation,
|
|
172
|
-
|
|
251
|
+
async carry(conversation, call) {
|
|
252
|
+
if (call.kind === 'open')
|
|
253
|
+
return { result: this.open(conversation, call) };
|
|
254
|
+
if (call.kind === 'message')
|
|
255
|
+
return this.message(conversation, call);
|
|
256
|
+
await this.launched(conversation);
|
|
257
|
+
if (call.kind === 'interrupt') {
|
|
173
258
|
if (--conversation.interrupts === 0)
|
|
174
259
|
conversation.interrupted = new AbortController();
|
|
175
|
-
await this.interrupt(conversation, String(
|
|
260
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
176
261
|
}
|
|
177
|
-
if (
|
|
262
|
+
if (call.kind === 'kill') {
|
|
178
263
|
await this.kill(conversation);
|
|
179
264
|
await conversation.sending;
|
|
180
265
|
if (--conversation.kills === 0)
|
|
181
266
|
conversation.killed = new AbortController();
|
|
182
267
|
}
|
|
183
|
-
if (
|
|
184
|
-
await this.option(conversation, String(
|
|
185
|
-
if (
|
|
186
|
-
const question = this.questions.get(String(
|
|
268
|
+
if (call.kind === 'option')
|
|
269
|
+
await this.option(conversation, String(call.option), call.value);
|
|
270
|
+
if (call.kind === 'answer') {
|
|
271
|
+
const question = this.questions.get(String(call.interaction_id));
|
|
187
272
|
if (question === undefined) {
|
|
188
|
-
const text = answerMessage(
|
|
189
|
-
return this.message(conversation, { ...
|
|
273
|
+
const text = answerMessage(call.request, call.response);
|
|
274
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
190
275
|
}
|
|
191
|
-
question.answer(
|
|
276
|
+
question.answer(call.response);
|
|
192
277
|
}
|
|
193
|
-
return {
|
|
278
|
+
return { result: Promise.resolve({}) };
|
|
194
279
|
}
|
|
195
|
-
|
|
280
|
+
open(conversation, call) {
|
|
196
281
|
if (conversation.running !== null)
|
|
197
|
-
return {};
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
282
|
+
return Promise.resolve({});
|
|
283
|
+
const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
|
|
284
|
+
return launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) }));
|
|
285
|
+
}
|
|
286
|
+
launch(conversation, route, sessionId, empty) {
|
|
287
|
+
let opened;
|
|
288
|
+
let failed;
|
|
289
|
+
const bridge = new Promise((resolve, reject) => {
|
|
290
|
+
opened = resolve;
|
|
291
|
+
failed = reject;
|
|
292
|
+
});
|
|
293
|
+
bridge.catch(() => undefined);
|
|
294
|
+
const session = this.start(conversation, route, sessionId, empty, opened);
|
|
295
|
+
session.catch(failed);
|
|
296
|
+
const launch = { bridge, session };
|
|
297
|
+
conversation.launch = launch;
|
|
298
|
+
void session
|
|
299
|
+
.catch(() => undefined)
|
|
300
|
+
.then(() => {
|
|
301
|
+
if (conversation.launch !== launch)
|
|
302
|
+
return;
|
|
303
|
+
conversation.launch = null;
|
|
304
|
+
this.closeIdleBridge(conversation);
|
|
305
|
+
this.next(conversation);
|
|
306
|
+
});
|
|
307
|
+
this.next(conversation);
|
|
308
|
+
return launch;
|
|
309
|
+
}
|
|
310
|
+
async launched(conversation) {
|
|
311
|
+
while (conversation.launch !== null)
|
|
312
|
+
await conversation.launch.session.catch(() => undefined);
|
|
313
|
+
}
|
|
314
|
+
instructed(conversation, route) {
|
|
315
|
+
const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
|
|
316
|
+
if (bridge === undefined)
|
|
317
|
+
return null;
|
|
318
|
+
const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
|
|
319
|
+
read.catch(() => undefined);
|
|
320
|
+
return read;
|
|
321
|
+
}
|
|
322
|
+
async message(conversation, call) {
|
|
323
|
+
const content = [{ type: 'text', text: String(call.text) }];
|
|
207
324
|
let route;
|
|
325
|
+
let instructing = null;
|
|
208
326
|
try {
|
|
209
|
-
route = await this.route(conversation,
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
content.push({ type: 'text', text: lines.join('\n') });
|
|
327
|
+
route = await this.route(conversation, call.agent_id);
|
|
328
|
+
if (call.first === true)
|
|
329
|
+
instructing = this.instructed(conversation, route);
|
|
330
|
+
if (call.message_id !== null)
|
|
331
|
+
content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
|
|
215
332
|
if (conversation.kills > 0)
|
|
216
333
|
throw new Error(KILLED);
|
|
217
334
|
}
|
|
218
335
|
catch (error) {
|
|
219
|
-
|
|
336
|
+
this.closeIdleBridge(conversation);
|
|
337
|
+
this.prepares(conversation, null);
|
|
338
|
+
return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
220
339
|
}
|
|
340
|
+
conversation.preparing = null;
|
|
221
341
|
return {
|
|
222
|
-
|
|
223
|
-
conversation.waiting.push({
|
|
342
|
+
result: new Promise((settle) => {
|
|
343
|
+
conversation.waiting.push({ call, route, content, instructing, settle });
|
|
224
344
|
this.next(conversation);
|
|
225
345
|
}),
|
|
226
346
|
};
|
|
@@ -229,62 +349,148 @@ export class Conversations {
|
|
|
229
349
|
if (conversation.sending !== null || conversation.open > 0)
|
|
230
350
|
return;
|
|
231
351
|
const waiting = conversation.waiting.shift();
|
|
232
|
-
if (waiting === undefined)
|
|
352
|
+
if (waiting === undefined) {
|
|
353
|
+
const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
|
|
354
|
+
if (conversation.starting !== step)
|
|
355
|
+
this.starts(conversation, step);
|
|
233
356
|
return;
|
|
357
|
+
}
|
|
234
358
|
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
235
359
|
conversation.sending = null;
|
|
236
360
|
this.next(conversation);
|
|
237
361
|
});
|
|
238
362
|
}
|
|
239
363
|
async send(conversation, waiting) {
|
|
240
|
-
const {
|
|
241
|
-
let
|
|
364
|
+
const { call } = waiting;
|
|
365
|
+
let result = {};
|
|
366
|
+
let started = false;
|
|
367
|
+
let instructing = waiting.instructing;
|
|
242
368
|
const prompt = [];
|
|
243
369
|
try {
|
|
244
370
|
if (conversation.running === null) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
371
|
+
this.starts(conversation, 'session');
|
|
372
|
+
await this.launched(conversation);
|
|
373
|
+
}
|
|
374
|
+
if (conversation.running === null) {
|
|
375
|
+
const stored = conversation.session ?? call.provider_session_id;
|
|
376
|
+
const empty = call.first === true && conversation.prompted !== stored;
|
|
377
|
+
const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
|
|
378
|
+
if (call.first === true)
|
|
379
|
+
instructing = this.instructed(conversation, waiting.route);
|
|
380
|
+
started = (await launch.session) !== stored;
|
|
249
381
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
382
|
+
const session = conversation.running.sessionId;
|
|
383
|
+
if (started || (call.first === true && session !== call.provider_session_id)) {
|
|
384
|
+
result = { provider_session_id: session };
|
|
385
|
+
}
|
|
386
|
+
if (call.first === true) {
|
|
387
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
255
388
|
}
|
|
256
389
|
if (conversation.kills > 0)
|
|
257
390
|
throw new Error(KILLED);
|
|
258
391
|
}
|
|
259
392
|
catch (error) {
|
|
260
|
-
|
|
393
|
+
this.starts(conversation, null);
|
|
394
|
+
if (started) {
|
|
261
395
|
conversation.session = null;
|
|
262
396
|
await this.kill(conversation);
|
|
263
397
|
}
|
|
264
|
-
|
|
398
|
+
const refused = refusal(conversation, error);
|
|
399
|
+
const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
|
|
400
|
+
waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
this.starts(conversation, null);
|
|
404
|
+
this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
|
|
405
|
+
waiting.settle(result);
|
|
406
|
+
}
|
|
407
|
+
starts(conversation, step) {
|
|
408
|
+
conversation.starting = step;
|
|
409
|
+
if (conversation.shown !== null)
|
|
410
|
+
clearTimeout(conversation.shown);
|
|
411
|
+
conversation.shown = null;
|
|
412
|
+
if (step === null || conversation.told !== null) {
|
|
413
|
+
this.tell(conversation, step);
|
|
265
414
|
return;
|
|
266
415
|
}
|
|
267
|
-
|
|
268
|
-
|
|
416
|
+
conversation.shown = setTimeout(() => {
|
|
417
|
+
conversation.shown = null;
|
|
418
|
+
if (conversation.starting === step)
|
|
419
|
+
this.tell(conversation, step);
|
|
420
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
269
421
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
422
|
+
prepares(conversation, step) {
|
|
423
|
+
conversation.preparing = step;
|
|
424
|
+
this.next(conversation);
|
|
425
|
+
}
|
|
426
|
+
tell(conversation, step) {
|
|
427
|
+
if (conversation.told === step && !conversation.untold)
|
|
428
|
+
return;
|
|
429
|
+
conversation.told = step;
|
|
430
|
+
this.retell(conversation);
|
|
431
|
+
}
|
|
432
|
+
retell(conversation) {
|
|
433
|
+
conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
|
|
434
|
+
}
|
|
435
|
+
async transcribed(conversation, messageId, text) {
|
|
436
|
+
const paths = linkedPaths(text);
|
|
437
|
+
if (paths.length === 0)
|
|
438
|
+
return [];
|
|
439
|
+
const killed = conversation.killed.signal;
|
|
440
|
+
const interrupted = conversation.interrupted.signal;
|
|
441
|
+
const transcribing = AbortSignal.any([killed, interrupted]);
|
|
442
|
+
let signal = killed;
|
|
443
|
+
const blocks = [];
|
|
444
|
+
try {
|
|
445
|
+
const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
|
|
446
|
+
const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
|
|
447
|
+
for (const path of paths) {
|
|
448
|
+
let answered = await read(path);
|
|
449
|
+
if (answered === null)
|
|
450
|
+
continue;
|
|
451
|
+
signal = transcribing;
|
|
452
|
+
this.prepares(conversation, 'transcription');
|
|
453
|
+
const said = await transcribe(async () => {
|
|
454
|
+
const address = answered ?? (await read(path));
|
|
455
|
+
answered = null;
|
|
456
|
+
if (address === null)
|
|
457
|
+
throw new Error(`${path} is no longer an audio file`);
|
|
458
|
+
return address;
|
|
459
|
+
}, signal);
|
|
460
|
+
const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
|
|
461
|
+
await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
|
|
462
|
+
blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
|
|
285
463
|
}
|
|
286
464
|
}
|
|
287
|
-
|
|
465
|
+
catch (error) {
|
|
466
|
+
if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
|
|
467
|
+
throw new NotStarted(INTERRUPTED, { cause: error });
|
|
468
|
+
throw error;
|
|
469
|
+
}
|
|
470
|
+
return blocks;
|
|
471
|
+
}
|
|
472
|
+
bridged(conversation) {
|
|
473
|
+
if (conversation.bridge === null) {
|
|
474
|
+
const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
475
|
+
conversation.bridge = bridging;
|
|
476
|
+
bridging.catch(() => {
|
|
477
|
+
if (conversation.bridge === bridging)
|
|
478
|
+
conversation.bridge = null;
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
return conversation.bridge;
|
|
482
|
+
}
|
|
483
|
+
closeBridge(conversation, bridging, bridge) {
|
|
484
|
+
bridge.close();
|
|
485
|
+
if (conversation.bridge === bridging)
|
|
486
|
+
conversation.bridge = null;
|
|
487
|
+
}
|
|
488
|
+
closeIdleBridge(conversation) {
|
|
489
|
+
const bridging = conversation.bridge;
|
|
490
|
+
if (bridging === null || conversation.running !== null || conversation.launch !== null)
|
|
491
|
+
return;
|
|
492
|
+
conversation.bridge = null;
|
|
493
|
+
void bridging.then((bridge) => bridge.close(), () => undefined);
|
|
288
494
|
}
|
|
289
495
|
async interrupt(conversation, turnId) {
|
|
290
496
|
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
@@ -295,11 +501,13 @@ export class Conversations {
|
|
|
295
501
|
}
|
|
296
502
|
halt(conversation) {
|
|
297
503
|
if (conversation.opening !== null)
|
|
298
|
-
|
|
504
|
+
killMarked(marker(conversation.opening));
|
|
505
|
+
for (const scope of conversation.leftovers)
|
|
506
|
+
scope.kill();
|
|
299
507
|
if (conversation.running === null)
|
|
300
508
|
return;
|
|
301
509
|
conversation.running.killed = true;
|
|
302
|
-
|
|
510
|
+
conversation.running.scope.kill();
|
|
303
511
|
}
|
|
304
512
|
async kill(conversation) {
|
|
305
513
|
const running = conversation.running;
|
|
@@ -337,53 +545,78 @@ export class Conversations {
|
|
|
337
545
|
}
|
|
338
546
|
return route;
|
|
339
547
|
}
|
|
340
|
-
async start(conversation,
|
|
548
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
549
|
+
const route = await routed;
|
|
341
550
|
const reader = readerFor(route.kind);
|
|
342
551
|
reader.session = sessionId;
|
|
343
552
|
const app = client({ name: '@agentshouse/kit' })
|
|
344
553
|
.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))
|
|
554
|
+
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
555
|
+
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
|
|
346
556
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
347
|
-
const
|
|
557
|
+
const bridging = this.bridged(conversation);
|
|
558
|
+
const bridge = await step(unbridged, bridging);
|
|
348
559
|
if (conversation.kills > 0) {
|
|
349
|
-
|
|
560
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
350
561
|
throw new Error(KILLED);
|
|
351
562
|
}
|
|
352
563
|
conversation.opening = bridge;
|
|
564
|
+
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
565
|
+
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
566
|
+
let adapter = null;
|
|
353
567
|
try {
|
|
354
|
-
|
|
355
|
-
await CLIS[route.kind].allowHouse();
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
568
|
+
bridged(bridge);
|
|
569
|
+
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
570
|
+
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
571
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
572
|
+
work.read(message);
|
|
573
|
+
this.read(conversation, reader, message);
|
|
574
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
575
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
359
576
|
const agent = adapter.connection.agent;
|
|
360
577
|
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 };
|
|
578
|
+
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
579
|
+
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
580
|
+
const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
365
581
|
reader.session = opened.sessionId;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
582
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
583
|
+
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
584
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
585
|
+
}
|
|
586
|
+
const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
587
|
+
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
588
|
+
void work.settled().then(() => scope.record());
|
|
369
589
|
conversation.running = running;
|
|
590
|
+
adapter.turn(conversation.open > 0);
|
|
370
591
|
conversation.session = running.sessionId;
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
this.options(conversation, options);
|
|
375
|
-
void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
|
|
592
|
+
conversation.options = options;
|
|
593
|
+
this.facts(conversation);
|
|
594
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
376
595
|
return running.sessionId;
|
|
377
596
|
}
|
|
378
597
|
catch (error) {
|
|
379
|
-
|
|
380
|
-
|
|
598
|
+
adapter?.retire();
|
|
599
|
+
work.close();
|
|
600
|
+
conversation.leftovers.add(scope);
|
|
601
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
381
602
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
382
603
|
}
|
|
383
604
|
finally {
|
|
384
605
|
conversation.opening = null;
|
|
385
606
|
}
|
|
386
607
|
}
|
|
608
|
+
async resume(conversation, kind, adapter, request, empty) {
|
|
609
|
+
try {
|
|
610
|
+
return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
|
|
614
|
+
throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
|
|
615
|
+
}
|
|
616
|
+
logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
387
620
|
async launchSettings(running, route, offered) {
|
|
388
621
|
let options = offered;
|
|
389
622
|
const settings = [
|
|
@@ -405,26 +638,37 @@ export class Conversations {
|
|
|
405
638
|
}
|
|
406
639
|
return options;
|
|
407
640
|
}
|
|
408
|
-
exited(conversation,
|
|
409
|
-
|
|
410
|
-
if (running?.adapter !== adapter)
|
|
411
|
-
return;
|
|
412
|
-
this.detach(conversation);
|
|
641
|
+
exited(conversation, running, cause) {
|
|
642
|
+
this.detach(conversation, running);
|
|
413
643
|
if (conversation.turn !== null) {
|
|
414
|
-
this.end(conversation, conversation.turn,
|
|
644
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
415
645
|
}
|
|
416
646
|
this.next(conversation);
|
|
417
647
|
this.kit.changed();
|
|
418
648
|
}
|
|
419
|
-
detach(conversation) {
|
|
649
|
+
detach(conversation, running) {
|
|
650
|
+
if (conversation.bridge === running.bridging)
|
|
651
|
+
conversation.bridge = null;
|
|
652
|
+
running.work.close();
|
|
653
|
+
conversation.leftovers.add(running.scope);
|
|
420
654
|
conversation.running = null;
|
|
421
655
|
conversation.commands = null;
|
|
422
|
-
conversation.
|
|
656
|
+
conversation.options = null;
|
|
423
657
|
for (const [id, question] of this.questions) {
|
|
424
658
|
if (question.conversation === conversation)
|
|
425
659
|
this.questions.delete(id);
|
|
426
660
|
}
|
|
427
|
-
this.
|
|
661
|
+
this.process(conversation, false);
|
|
662
|
+
}
|
|
663
|
+
process(conversation, running) {
|
|
664
|
+
conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
|
|
665
|
+
}
|
|
666
|
+
facts(conversation) {
|
|
667
|
+
this.process(conversation, true);
|
|
668
|
+
if (conversation.commands !== null)
|
|
669
|
+
this.commands(conversation, conversation.commands);
|
|
670
|
+
if (conversation.options !== null)
|
|
671
|
+
this.options(conversation, conversation.options);
|
|
428
672
|
}
|
|
429
673
|
commands(conversation, commands) {
|
|
430
674
|
conversation.commands = commands;
|
|
@@ -435,6 +679,7 @@ export class Conversations {
|
|
|
435
679
|
options(conversation, options) {
|
|
436
680
|
if (conversation.running === null)
|
|
437
681
|
return;
|
|
682
|
+
conversation.options = options;
|
|
438
683
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
439
684
|
}
|
|
440
685
|
deliver(conversation, turn, event, body) {
|
|
@@ -449,7 +694,7 @@ export class Conversations {
|
|
|
449
694
|
return delivered;
|
|
450
695
|
}
|
|
451
696
|
write(conversation, turn, operation) {
|
|
452
|
-
if (
|
|
697
|
+
if (turn.ended || !this.watched)
|
|
453
698
|
return;
|
|
454
699
|
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
455
700
|
}
|
|
@@ -460,61 +705,74 @@ export class Conversations {
|
|
|
460
705
|
begin(conversation) {
|
|
461
706
|
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
462
707
|
conversation.open++;
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
turn.admitted = true;
|
|
466
|
-
this.replay(conversation, turn);
|
|
467
|
-
})
|
|
468
|
-
.catch(logged);
|
|
708
|
+
conversation.running?.adapter.turn(true);
|
|
709
|
+
void this.deliver(conversation, turn, 'started', {}).catch(logged);
|
|
469
710
|
conversation.turn = turn;
|
|
470
711
|
if (conversation.context !== null)
|
|
471
712
|
turn.parts.measure(conversation.context);
|
|
472
713
|
for (const part of conversation.held.splice(0))
|
|
473
714
|
turn.parts.start(part);
|
|
474
|
-
for (const event of conversation.running?.reader.turnStarted() ?? [])
|
|
475
|
-
this.event(conversation, event);
|
|
476
715
|
this.kit.changed();
|
|
477
716
|
return turn;
|
|
478
717
|
}
|
|
479
|
-
end(conversation, turn, failed) {
|
|
718
|
+
end(conversation, turn, failed, notice = null) {
|
|
480
719
|
if (turn.ended)
|
|
481
720
|
return;
|
|
721
|
+
if (failed !== null)
|
|
722
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
482
723
|
if (conversation.turn === turn) {
|
|
483
724
|
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
484
725
|
this.apply(conversation, turn, event);
|
|
485
726
|
}
|
|
727
|
+
if (notice !== null)
|
|
728
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
486
729
|
turn.ended = true;
|
|
487
730
|
conversation.open--;
|
|
731
|
+
conversation.running?.adapter.turn(conversation.open > 0);
|
|
488
732
|
if (conversation.turn === turn)
|
|
489
733
|
conversation.turn = null;
|
|
490
734
|
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
491
|
-
parts: turn.parts.
|
|
735
|
+
parts: turn.parts.saved(),
|
|
492
736
|
context: turn.parts.context,
|
|
493
|
-
...failed,
|
|
737
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
494
738
|
}));
|
|
495
739
|
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
496
740
|
this.next(conversation);
|
|
497
741
|
this.kit.changed();
|
|
498
742
|
}
|
|
499
|
-
prompt(conversation, prompt) {
|
|
743
|
+
prompt(conversation, prompt, taken) {
|
|
500
744
|
const running = conversation.running;
|
|
501
745
|
const turn = this.begin(conversation);
|
|
502
746
|
turn.prompted = true;
|
|
503
|
-
|
|
747
|
+
turn.taken = taken;
|
|
748
|
+
conversation.prompted = running.sessionId;
|
|
749
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
750
|
+
this.took(turn);
|
|
751
|
+
const notice = answeredFailure(answer);
|
|
752
|
+
if (notice !== null) {
|
|
753
|
+
this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
757
|
+
}
|
|
758
|
+
}, async (error) => {
|
|
504
759
|
if (running.adapter.connection.signal.aborted) {
|
|
505
|
-
this.end(conversation, turn,
|
|
760
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
506
761
|
}
|
|
507
762
|
else {
|
|
763
|
+
this.took(turn);
|
|
508
764
|
if (conversation.open === 1 && conversation.running === running) {
|
|
509
|
-
|
|
510
|
-
this.detach(conversation);
|
|
765
|
+
running.adapter.retire();
|
|
766
|
+
this.detach(conversation, running);
|
|
511
767
|
}
|
|
512
|
-
this.end(conversation, turn,
|
|
768
|
+
this.end(conversation, turn, answeredWith(error));
|
|
513
769
|
}
|
|
514
770
|
});
|
|
515
771
|
}
|
|
516
772
|
started(conversation) {
|
|
517
|
-
|
|
773
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
774
|
+
this.took(turn);
|
|
775
|
+
return turn;
|
|
518
776
|
}
|
|
519
777
|
ended(conversation) {
|
|
520
778
|
const turn = conversation.turn;
|
|
@@ -592,20 +850,6 @@ export class Conversations {
|
|
|
592
850
|
case 'options':
|
|
593
851
|
this.options(conversation, event.options);
|
|
594
852
|
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
853
|
case 'context':
|
|
610
854
|
conversation.context = { used: event.used, window: event.window };
|
|
611
855
|
conversation.turn?.parts.measure(conversation.context);
|
|
@@ -623,6 +867,11 @@ export class Conversations {
|
|
|
623
867
|
}
|
|
624
868
|
this.apply(conversation, this.started(conversation), event);
|
|
625
869
|
}
|
|
870
|
+
took(turn) {
|
|
871
|
+
const taken = turn.taken;
|
|
872
|
+
turn.taken = null;
|
|
873
|
+
taken?.();
|
|
874
|
+
}
|
|
626
875
|
apply(conversation, turn, event) {
|
|
627
876
|
switch (event.kind) {
|
|
628
877
|
case 'text':
|
|
@@ -647,7 +896,7 @@ export class Conversations {
|
|
|
647
896
|
turn.parts.plan(event.entries);
|
|
648
897
|
return;
|
|
649
898
|
case 'image':
|
|
650
|
-
turn.uploads.push(this.image(conversation, turn, event.data, event.mediaType));
|
|
899
|
+
turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
|
|
651
900
|
this.kit.changed();
|
|
652
901
|
return;
|
|
653
902
|
}
|
|
@@ -667,23 +916,30 @@ export class Conversations {
|
|
|
667
916
|
else
|
|
668
917
|
conversation.turn.parts.start(part);
|
|
669
918
|
}
|
|
670
|
-
async image(conversation, turn, data, mediaType) {
|
|
919
|
+
async image(conversation, turn, reserved, data, mediaType) {
|
|
671
920
|
const bytes = Buffer.from(data, 'base64');
|
|
672
921
|
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
922
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
673
923
|
try {
|
|
674
924
|
const declared = (await this.deliver(conversation, turn, 'images', {
|
|
675
925
|
name,
|
|
676
926
|
media_type: mediaType,
|
|
677
927
|
bytes: bytes.length,
|
|
678
|
-
sha256
|
|
928
|
+
sha256,
|
|
679
929
|
}));
|
|
680
|
-
const saved =
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
930
|
+
const saved = await uploaded(declared.upload, {
|
|
931
|
+
name,
|
|
932
|
+
bytes: bytes.length,
|
|
933
|
+
sha256,
|
|
934
|
+
read: (start, end) => [bytes.subarray(start, end)],
|
|
935
|
+
send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
|
|
936
|
+
finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
|
|
937
|
+
});
|
|
938
|
+
turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
|
|
684
939
|
}
|
|
685
940
|
catch (error) {
|
|
686
|
-
|
|
941
|
+
const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
942
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
|
|
687
943
|
}
|
|
688
944
|
}
|
|
689
945
|
}
|