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