@agentshouse/kit 0.2.1-c3895fcff473b → 0.2.1-c3a3042f56d6a
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 +97 -107
- package/dist/agents.js +162 -44
- package/dist/api.js +34 -2
- package/dist/app-git.js +48 -0
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +37 -58
- package/dist/building.js +46 -0
- package/dist/calls.js +71 -0
- package/dist/chatgpt.js +32 -0
- package/dist/clis-main.js +2 -2
- package/dist/clis.js +160 -80
- package/dist/codex-main.js +58 -0
- package/dist/connection.js +116 -0
- package/dist/conversations.js +629 -325
- package/dist/copies.js +188 -128
- package/dist/end.js +27 -0
- package/dist/enrol-main.js +7 -1
- package/dist/events.js +525 -0
- package/dist/files.js +139 -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 +119 -104
- package/dist/logout.js +14 -13
- package/dist/mcp.js +6 -2
- package/dist/parts.js +82 -0
- package/dist/placement.js +39 -0
- package/dist/refusals.js +12 -0
- package/dist/relay.js +33 -2
- package/dist/resident.js +118 -51
- package/dist/scope.js +206 -0
- package/dist/scripts.js +255 -0
- package/dist/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/silence.js +28 -0
- package/dist/skills.js +49 -35
- package/dist/transcription.js +129 -41
- package/dist/translate.js +2 -8
- package/dist/update.js +117 -53
- 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 -47
- 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,142 @@
|
|
|
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
|
+
if (running.killed)
|
|
77
|
+
return { failure: 'killed', cause: KILLED };
|
|
78
|
+
return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
|
|
79
|
+
}
|
|
20
80
|
function answerMessage(request, response) {
|
|
21
81
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
22
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
|
+
}
|
|
23
89
|
function logged(error) {
|
|
24
90
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
25
91
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
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
|
+
}
|
|
105
|
+
function marker(bridge) {
|
|
106
|
+
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
28
107
|
}
|
|
29
108
|
class Turn {
|
|
30
109
|
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;
|
|
110
|
+
parts;
|
|
40
111
|
questions = 0;
|
|
41
112
|
secrets = 0;
|
|
42
|
-
|
|
113
|
+
prompted = false;
|
|
114
|
+
taken = null;
|
|
43
115
|
ended = false;
|
|
116
|
+
images = 0;
|
|
117
|
+
uploads = [];
|
|
118
|
+
constructor(emit) {
|
|
119
|
+
this.parts = new Parts(emit);
|
|
120
|
+
}
|
|
44
121
|
}
|
|
45
122
|
class Conversation {
|
|
46
123
|
id;
|
|
47
124
|
running = null;
|
|
125
|
+
session = null;
|
|
48
126
|
turn = null;
|
|
49
127
|
commands = null;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
128
|
+
options = null;
|
|
129
|
+
exitUntold = false;
|
|
130
|
+
context = null;
|
|
131
|
+
leftovers = new Set();
|
|
132
|
+
waiting = [];
|
|
133
|
+
sending = null;
|
|
134
|
+
subagents = new Map();
|
|
135
|
+
held = [];
|
|
53
136
|
opening = null;
|
|
137
|
+
bridge = null;
|
|
138
|
+
launch = null;
|
|
139
|
+
prompted = null;
|
|
54
140
|
kills = 0;
|
|
55
141
|
killed = new AbortController();
|
|
56
142
|
interrupts = 0;
|
|
@@ -58,6 +144,11 @@ class Conversation {
|
|
|
58
144
|
open = 0;
|
|
59
145
|
queue = Promise.resolve();
|
|
60
146
|
reports = Promise.resolve();
|
|
147
|
+
starting = null;
|
|
148
|
+
preparing = null;
|
|
149
|
+
told = null;
|
|
150
|
+
untold = false;
|
|
151
|
+
shown = null;
|
|
61
152
|
constructor(id) {
|
|
62
153
|
this.id = id;
|
|
63
154
|
}
|
|
@@ -73,55 +164,80 @@ function asksSecret(request) {
|
|
|
73
164
|
export class Conversations {
|
|
74
165
|
kit;
|
|
75
166
|
conversations = new Map();
|
|
76
|
-
acks = new Map();
|
|
77
167
|
questions = new Map();
|
|
78
|
-
carrying = 0;
|
|
79
168
|
reporting = 0;
|
|
169
|
+
watched = false;
|
|
80
170
|
constructor(kit) {
|
|
81
171
|
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();
|
|
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 }));
|
|
108
192
|
});
|
|
109
193
|
}
|
|
110
194
|
idle() {
|
|
111
|
-
if (this.
|
|
195
|
+
if (this.reporting > 0)
|
|
112
196
|
return false;
|
|
113
197
|
for (const conversation of this.conversations.values()) {
|
|
114
198
|
const turn = conversation.turn;
|
|
115
199
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
116
|
-
if (conversation.
|
|
200
|
+
if (conversation.open > (asking ? 1 : 0))
|
|
117
201
|
return false;
|
|
118
202
|
}
|
|
119
|
-
|
|
203
|
+
const conversations = [...this.conversations.values()];
|
|
204
|
+
if (conversations.some((conversation) => conversation.running?.work.busy()))
|
|
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
|
+
}
|
|
219
|
+
}
|
|
220
|
+
this.kit.changed();
|
|
120
221
|
}
|
|
121
222
|
opened() {
|
|
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
|
+
}
|
|
232
|
+
}
|
|
233
|
+
watching(watched) {
|
|
234
|
+
const resend = watched && !this.watched;
|
|
235
|
+
this.watched = watched;
|
|
236
|
+
if (!resend)
|
|
237
|
+
return;
|
|
122
238
|
for (const conversation of this.conversations.values()) {
|
|
123
239
|
if (conversation.turn !== null)
|
|
124
|
-
conversation.turn
|
|
240
|
+
this.replay(conversation, conversation.turn);
|
|
125
241
|
}
|
|
126
242
|
}
|
|
127
243
|
conversation(id) {
|
|
@@ -132,93 +248,249 @@ export class Conversations {
|
|
|
132
248
|
}
|
|
133
249
|
return conversation;
|
|
134
250
|
}
|
|
135
|
-
async carry(conversation,
|
|
136
|
-
if (
|
|
137
|
-
return this.open(conversation,
|
|
138
|
-
if (
|
|
139
|
-
return this.message(conversation,
|
|
140
|
-
|
|
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') {
|
|
141
258
|
if (--conversation.interrupts === 0)
|
|
142
259
|
conversation.interrupted = new AbortController();
|
|
143
|
-
await this.interrupt(conversation, String(
|
|
260
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
144
261
|
}
|
|
145
|
-
if (
|
|
262
|
+
if (call.kind === 'kill') {
|
|
146
263
|
await this.kill(conversation);
|
|
264
|
+
await conversation.sending;
|
|
147
265
|
if (--conversation.kills === 0)
|
|
148
266
|
conversation.killed = new AbortController();
|
|
149
267
|
}
|
|
150
|
-
if (
|
|
151
|
-
await this.option(conversation, String(
|
|
152
|
-
if (
|
|
153
|
-
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));
|
|
154
272
|
if (question === undefined) {
|
|
155
|
-
|
|
273
|
+
const text = answerMessage(call.request, call.response);
|
|
274
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
156
275
|
}
|
|
157
|
-
question.answer(
|
|
276
|
+
question.answer(call.response);
|
|
158
277
|
}
|
|
159
|
-
return {};
|
|
278
|
+
return { result: Promise.resolve({}) };
|
|
160
279
|
}
|
|
161
|
-
|
|
280
|
+
open(conversation, call) {
|
|
162
281
|
if (conversation.running !== null)
|
|
163
|
-
return {};
|
|
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) }];
|
|
324
|
+
let route;
|
|
325
|
+
let instructing = null;
|
|
164
326
|
try {
|
|
165
|
-
|
|
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))));
|
|
332
|
+
if (conversation.kills > 0)
|
|
333
|
+
throw new Error(KILLED);
|
|
166
334
|
}
|
|
167
335
|
catch (error) {
|
|
168
|
-
|
|
336
|
+
this.closeIdleBridge(conversation);
|
|
337
|
+
this.prepares(conversation, null);
|
|
338
|
+
return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
339
|
+
}
|
|
340
|
+
conversation.preparing = null;
|
|
341
|
+
return {
|
|
342
|
+
result: new Promise((settle) => {
|
|
343
|
+
conversation.waiting.push({ call, route, content, instructing, settle });
|
|
344
|
+
this.next(conversation);
|
|
345
|
+
}),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
next(conversation) {
|
|
349
|
+
if (conversation.sending !== null || conversation.open > 0)
|
|
350
|
+
return;
|
|
351
|
+
const waiting = conversation.waiting.shift();
|
|
352
|
+
if (waiting === undefined) {
|
|
353
|
+
const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
|
|
354
|
+
if (conversation.starting !== step)
|
|
355
|
+
this.starts(conversation, step);
|
|
356
|
+
return;
|
|
169
357
|
}
|
|
358
|
+
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
359
|
+
conversation.sending = null;
|
|
360
|
+
this.next(conversation);
|
|
361
|
+
});
|
|
170
362
|
}
|
|
171
|
-
async
|
|
172
|
-
|
|
363
|
+
async send(conversation, waiting) {
|
|
364
|
+
const { call } = waiting;
|
|
365
|
+
let result = {};
|
|
366
|
+
let started = false;
|
|
367
|
+
let instructing = waiting.instructing;
|
|
173
368
|
const prompt = [];
|
|
174
369
|
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
370
|
if (conversation.running === null) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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;
|
|
381
|
+
}
|
|
382
|
+
const session = conversation.running.sessionId;
|
|
383
|
+
if (started || (call.first === true && session !== call.provider_session_id)) {
|
|
384
|
+
result = { provider_session_id: session };
|
|
183
385
|
}
|
|
184
|
-
if (
|
|
185
|
-
prompt.push({
|
|
186
|
-
type: 'text',
|
|
187
|
-
text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
|
|
188
|
-
});
|
|
386
|
+
if (call.first === true) {
|
|
387
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
189
388
|
}
|
|
190
|
-
prompt.push({ type: 'text', text: String(input.text) });
|
|
191
|
-
if (lines.length > 0)
|
|
192
|
-
prompt.push({ type: 'text', text: lines.join('\n') });
|
|
193
389
|
if (conversation.kills > 0)
|
|
194
390
|
throw new Error(KILLED);
|
|
195
391
|
}
|
|
196
392
|
catch (error) {
|
|
197
|
-
|
|
393
|
+
this.starts(conversation, null);
|
|
394
|
+
if (started) {
|
|
395
|
+
conversation.session = null;
|
|
198
396
|
await this.kill(conversation);
|
|
199
|
-
|
|
397
|
+
}
|
|
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);
|
|
414
|
+
return;
|
|
200
415
|
}
|
|
201
|
-
|
|
202
|
-
|
|
416
|
+
conversation.shown = setTimeout(() => {
|
|
417
|
+
conversation.shown = null;
|
|
418
|
+
if (conversation.starting === step)
|
|
419
|
+
this.tell(conversation, step);
|
|
420
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
203
421
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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}` });
|
|
219
463
|
}
|
|
220
464
|
}
|
|
221
|
-
|
|
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);
|
|
222
494
|
}
|
|
223
495
|
async interrupt(conversation, turnId) {
|
|
224
496
|
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
@@ -229,11 +501,13 @@ export class Conversations {
|
|
|
229
501
|
}
|
|
230
502
|
halt(conversation) {
|
|
231
503
|
if (conversation.opening !== null)
|
|
232
|
-
|
|
504
|
+
killMarked(marker(conversation.opening));
|
|
505
|
+
for (const scope of conversation.leftovers)
|
|
506
|
+
scope.kill();
|
|
233
507
|
if (conversation.running === null)
|
|
234
508
|
return;
|
|
235
509
|
conversation.running.killed = true;
|
|
236
|
-
|
|
510
|
+
conversation.running.scope.kill();
|
|
237
511
|
}
|
|
238
512
|
async kill(conversation) {
|
|
239
513
|
const running = conversation.running;
|
|
@@ -245,6 +519,7 @@ export class Conversations {
|
|
|
245
519
|
if (running === null)
|
|
246
520
|
return;
|
|
247
521
|
try {
|
|
522
|
+
running.reader.requested(option, value);
|
|
248
523
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
249
524
|
sessionId: running.sessionId,
|
|
250
525
|
configId: option,
|
|
@@ -253,6 +528,7 @@ export class Conversations {
|
|
|
253
528
|
this.options(conversation, answered.configOptions);
|
|
254
529
|
}
|
|
255
530
|
catch (error) {
|
|
531
|
+
running.reader.refused(option);
|
|
256
532
|
logged(error);
|
|
257
533
|
}
|
|
258
534
|
}
|
|
@@ -269,57 +545,78 @@ export class Conversations {
|
|
|
269
545
|
}
|
|
270
546
|
return route;
|
|
271
547
|
}
|
|
272
|
-
async start(conversation,
|
|
548
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
549
|
+
const route = await routed;
|
|
550
|
+
const reader = readerFor(route.kind);
|
|
551
|
+
reader.session = sessionId;
|
|
273
552
|
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
553
|
.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))
|
|
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));
|
|
279
556
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
280
|
-
const
|
|
557
|
+
const bridging = this.bridged(conversation);
|
|
558
|
+
const bridge = await step(unbridged, bridging);
|
|
281
559
|
if (conversation.kills > 0) {
|
|
282
|
-
|
|
560
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
283
561
|
throw new Error(KILLED);
|
|
284
562
|
}
|
|
285
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;
|
|
286
567
|
try {
|
|
287
|
-
|
|
288
|
-
await CLIS[route.kind].allowHouse();
|
|
289
|
-
const
|
|
290
|
-
|
|
568
|
+
bridged(bridge);
|
|
569
|
+
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
570
|
+
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
571
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
572
|
+
work.read(message);
|
|
573
|
+
this.read(conversation, reader, message);
|
|
574
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
575
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
291
576
|
const agent = adapter.connection.agent;
|
|
292
577
|
const cwd = route.working_directory;
|
|
293
|
-
const _meta = CLIS[route.kind].sessionMeta;
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
};
|
|
305
|
-
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
578
|
+
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
579
|
+
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
580
|
+
const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
581
|
+
reader.session = opened.sessionId;
|
|
582
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
583
|
+
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
584
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
585
|
+
}
|
|
586
|
+
const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
587
|
+
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
588
|
+
void work.settled().then(() => scope.record());
|
|
306
589
|
conversation.running = running;
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
this.
|
|
311
|
-
void adapter.
|
|
590
|
+
adapter.turn(conversation.open > 0);
|
|
591
|
+
conversation.session = running.sessionId;
|
|
592
|
+
conversation.options = options;
|
|
593
|
+
this.facts(conversation);
|
|
594
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
312
595
|
return running.sessionId;
|
|
313
596
|
}
|
|
314
597
|
catch (error) {
|
|
315
|
-
|
|
316
|
-
|
|
598
|
+
adapter?.retire();
|
|
599
|
+
work.close();
|
|
600
|
+
conversation.leftovers.add(scope);
|
|
601
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
317
602
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
318
603
|
}
|
|
319
604
|
finally {
|
|
320
605
|
conversation.opening = null;
|
|
321
606
|
}
|
|
322
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
|
+
}
|
|
323
620
|
async launchSettings(running, route, offered) {
|
|
324
621
|
let options = offered;
|
|
325
622
|
const settings = [
|
|
@@ -331,6 +628,7 @@ export class Conversations {
|
|
|
331
628
|
if (value === null)
|
|
332
629
|
continue;
|
|
333
630
|
const option = options.find((candidate) => candidate.category === category);
|
|
631
|
+
running.reader.requested(option?.id ?? category, value);
|
|
334
632
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
335
633
|
sessionId: running.sessionId,
|
|
336
634
|
configId: option?.id ?? category,
|
|
@@ -340,23 +638,37 @@ export class Conversations {
|
|
|
340
638
|
}
|
|
341
639
|
return options;
|
|
342
640
|
}
|
|
343
|
-
exited(conversation,
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
641
|
+
exited(conversation, running, cause) {
|
|
642
|
+
this.detach(conversation, running);
|
|
643
|
+
if (conversation.turn !== null) {
|
|
644
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
645
|
+
}
|
|
646
|
+
this.next(conversation);
|
|
647
|
+
this.kit.changed();
|
|
648
|
+
}
|
|
649
|
+
detach(conversation, running) {
|
|
650
|
+
if (conversation.bridge === running.bridging)
|
|
651
|
+
conversation.bridge = null;
|
|
652
|
+
running.work.close();
|
|
653
|
+
conversation.leftovers.add(running.scope);
|
|
347
654
|
conversation.running = null;
|
|
348
655
|
conversation.commands = null;
|
|
349
|
-
conversation.
|
|
350
|
-
conversation.queued.length = 0;
|
|
656
|
+
conversation.options = null;
|
|
351
657
|
for (const [id, question] of this.questions) {
|
|
352
658
|
if (question.conversation === conversation)
|
|
353
659
|
this.questions.delete(id);
|
|
354
660
|
}
|
|
355
|
-
this.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
}
|
|
359
|
-
|
|
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);
|
|
360
672
|
}
|
|
361
673
|
commands(conversation, commands) {
|
|
362
674
|
conversation.commands = commands;
|
|
@@ -367,11 +679,12 @@ export class Conversations {
|
|
|
367
679
|
options(conversation, options) {
|
|
368
680
|
if (conversation.running === null)
|
|
369
681
|
return;
|
|
682
|
+
conversation.options = options;
|
|
370
683
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
371
684
|
}
|
|
372
685
|
deliver(conversation, turn, event, body) {
|
|
373
686
|
this.reporting++;
|
|
374
|
-
const delivered = conversation.reports.then(() => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, body));
|
|
687
|
+
const delivered = conversation.reports.then(async () => this.kit.house.deliver(`/kit/conversations/${conversation.id}/turns/${turn.id}/${event}`, await body));
|
|
375
688
|
conversation.reports = delivered
|
|
376
689
|
.catch(() => undefined)
|
|
377
690
|
.finally(() => {
|
|
@@ -380,119 +693,97 @@ export class Conversations {
|
|
|
380
693
|
});
|
|
381
694
|
return delivered;
|
|
382
695
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
.then(() => this.admitted(conversation, turn))
|
|
388
|
-
.catch(logged);
|
|
389
|
-
return turn;
|
|
696
|
+
write(conversation, turn, operation) {
|
|
697
|
+
if (turn.ended || !this.watched)
|
|
698
|
+
return;
|
|
699
|
+
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
390
700
|
}
|
|
391
|
-
|
|
392
|
-
turn.
|
|
393
|
-
|
|
394
|
-
this.kit.send(note);
|
|
395
|
-
if (turn.text !== '') {
|
|
396
|
-
turn.unsentFrom = 0;
|
|
397
|
-
this.draft(conversation, turn);
|
|
398
|
-
}
|
|
399
|
-
if (turn.entries !== null)
|
|
400
|
-
this.plan(conversation, turn);
|
|
701
|
+
replay(conversation, turn) {
|
|
702
|
+
for (const operation of turn.parts.operations())
|
|
703
|
+
this.write(conversation, turn, operation);
|
|
401
704
|
}
|
|
402
705
|
begin(conversation) {
|
|
403
|
-
const turn = this.
|
|
706
|
+
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
707
|
+
conversation.open++;
|
|
708
|
+
conversation.running?.adapter.turn(true);
|
|
709
|
+
void this.deliver(conversation, turn, 'started', {}).catch(logged);
|
|
404
710
|
conversation.turn = turn;
|
|
711
|
+
if (conversation.context !== null)
|
|
712
|
+
turn.parts.measure(conversation.context);
|
|
713
|
+
for (const part of conversation.held.splice(0))
|
|
714
|
+
turn.parts.start(part);
|
|
405
715
|
this.kit.changed();
|
|
406
716
|
return turn;
|
|
407
717
|
}
|
|
408
|
-
|
|
409
|
-
if (turn.ended)
|
|
410
|
-
return;
|
|
411
|
-
const segment = turn.segment;
|
|
412
|
-
turn.segment = null;
|
|
413
|
-
if (segment?.phase === 'note')
|
|
414
|
-
this.note(conversation, turn, segment.text);
|
|
415
|
-
else if (segment !== null)
|
|
416
|
-
this.answer(conversation, turn, segment.text);
|
|
417
|
-
this.end(conversation, turn, { text: turn.text });
|
|
418
|
-
}
|
|
419
|
-
end(conversation, turn, outcome) {
|
|
718
|
+
end(conversation, turn, failed, notice = null) {
|
|
420
719
|
if (turn.ended)
|
|
421
720
|
return;
|
|
721
|
+
if (failed !== null)
|
|
722
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
723
|
+
if (conversation.turn === turn) {
|
|
724
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
725
|
+
this.apply(conversation, turn, event);
|
|
726
|
+
}
|
|
727
|
+
if (notice !== null)
|
|
728
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
422
729
|
turn.ended = true;
|
|
423
730
|
conversation.open--;
|
|
731
|
+
conversation.running?.adapter.turn(conversation.open > 0);
|
|
424
732
|
if (conversation.turn === turn)
|
|
425
733
|
conversation.turn = null;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
734
|
+
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
735
|
+
parts: turn.parts.saved(),
|
|
736
|
+
context: turn.parts.context,
|
|
737
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
738
|
+
}));
|
|
739
|
+
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
740
|
+
this.next(conversation);
|
|
430
741
|
this.kit.changed();
|
|
431
742
|
}
|
|
432
|
-
|
|
433
|
-
if (conversation.open > 0)
|
|
434
|
-
conversation.late.push(cause);
|
|
435
|
-
else
|
|
436
|
-
this.end(conversation, this.report(conversation), { failed: cause });
|
|
437
|
-
}
|
|
438
|
-
prompt(conversation, prompt) {
|
|
743
|
+
prompt(conversation, prompt, taken) {
|
|
439
744
|
const running = conversation.running;
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
this.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
else
|
|
451
|
-
this.end(conversation,
|
|
745
|
+
const turn = this.begin(conversation);
|
|
746
|
+
turn.prompted = true;
|
|
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) => {
|
|
759
|
+
if (running.adapter.connection.signal.aborted) {
|
|
760
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
this.took(turn);
|
|
764
|
+
if (conversation.open === 1 && conversation.running === running) {
|
|
765
|
+
running.adapter.retire();
|
|
766
|
+
this.detach(conversation, running);
|
|
767
|
+
}
|
|
768
|
+
this.end(conversation, turn, answeredWith(error));
|
|
452
769
|
}
|
|
453
770
|
});
|
|
454
771
|
}
|
|
455
|
-
attach(turn, sent) {
|
|
456
|
-
turn.prompt = sent;
|
|
457
|
-
sent.turn = turn;
|
|
458
|
-
}
|
|
459
|
-
answered(conversation, sent, failed) {
|
|
460
|
-
const turn = sent.turn;
|
|
461
|
-
if (turn === null) {
|
|
462
|
-
const queued = conversation.queued.indexOf(sent);
|
|
463
|
-
if (queued < 0)
|
|
464
|
-
return;
|
|
465
|
-
conversation.queued.splice(queued, 1);
|
|
466
|
-
if (failed !== null)
|
|
467
|
-
this.failed(conversation, failed);
|
|
468
|
-
}
|
|
469
|
-
else if (failed !== null) {
|
|
470
|
-
if (turn.ended)
|
|
471
|
-
this.failed(conversation, failed);
|
|
472
|
-
else
|
|
473
|
-
this.end(conversation, turn, { failed });
|
|
474
|
-
}
|
|
475
|
-
else if (turn.prompt === sent) {
|
|
476
|
-
this.finish(conversation, turn);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
772
|
started(conversation) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const turn = this.begin(conversation);
|
|
483
|
-
const queued = conversation.queued.shift();
|
|
484
|
-
if (queued !== undefined)
|
|
485
|
-
this.attach(turn, queued);
|
|
773
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
774
|
+
this.took(turn);
|
|
486
775
|
return turn;
|
|
487
776
|
}
|
|
488
777
|
ended(conversation) {
|
|
489
778
|
const turn = conversation.turn;
|
|
490
779
|
if (turn === null)
|
|
491
780
|
return;
|
|
492
|
-
if (turn.
|
|
493
|
-
this.
|
|
781
|
+
if (!turn.prompted) {
|
|
782
|
+
this.end(conversation, turn, null);
|
|
494
783
|
}
|
|
495
784
|
else {
|
|
785
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
786
|
+
this.apply(conversation, turn, event);
|
|
496
787
|
conversation.turn = null;
|
|
497
788
|
this.kit.changed();
|
|
498
789
|
}
|
|
@@ -541,101 +832,114 @@ export class Conversations {
|
|
|
541
832
|
if (content !== null)
|
|
542
833
|
this.questions.get(interactionId)?.answer({ action: 'accept', content });
|
|
543
834
|
}
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
835
|
+
read(conversation, reader, message) {
|
|
836
|
+
for (const event of reader.read(message))
|
|
837
|
+
this.event(conversation, event);
|
|
838
|
+
}
|
|
839
|
+
event(conversation, event) {
|
|
840
|
+
switch (event.kind) {
|
|
841
|
+
case 'started':
|
|
842
|
+
this.started(conversation);
|
|
843
|
+
return;
|
|
844
|
+
case 'ended':
|
|
845
|
+
this.ended(conversation);
|
|
846
|
+
return;
|
|
847
|
+
case 'commands':
|
|
848
|
+
this.commands(conversation, event.commands);
|
|
849
|
+
return;
|
|
850
|
+
case 'options':
|
|
851
|
+
this.options(conversation, event.options);
|
|
852
|
+
return;
|
|
853
|
+
case 'context':
|
|
854
|
+
conversation.context = { used: event.used, window: event.window };
|
|
855
|
+
conversation.turn?.parts.measure(conversation.context);
|
|
856
|
+
return;
|
|
857
|
+
case 'subagent-done':
|
|
858
|
+
this.subagentDone(conversation, event.id, event.result);
|
|
859
|
+
return;
|
|
860
|
+
case 'marker':
|
|
861
|
+
case 'image':
|
|
862
|
+
if (conversation.turn !== null)
|
|
863
|
+
break;
|
|
864
|
+
if (event.kind === 'marker' && event.marker === 'job')
|
|
865
|
+
conversation.held.push({ type: 'marker', marker: 'job', text: event.text });
|
|
866
|
+
return;
|
|
568
867
|
}
|
|
868
|
+
this.apply(conversation, this.started(conversation), event);
|
|
569
869
|
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
conversation.jobs.delete(job.id);
|
|
575
|
-
this.kit.changed();
|
|
870
|
+
took(turn) {
|
|
871
|
+
const taken = turn.taken;
|
|
872
|
+
turn.taken = null;
|
|
873
|
+
taken?.();
|
|
576
874
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
875
|
+
apply(conversation, turn, event) {
|
|
876
|
+
switch (event.kind) {
|
|
877
|
+
case 'text':
|
|
878
|
+
case 'reasoning':
|
|
879
|
+
turn.parts.write(event.kind, event.id, event.text);
|
|
880
|
+
return;
|
|
881
|
+
case 'command':
|
|
882
|
+
turn.parts.command();
|
|
883
|
+
return;
|
|
884
|
+
case 'subagent': {
|
|
885
|
+
const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
|
|
886
|
+
conversation.subagents.set(event.id, { description: event.description, turn, index });
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
case 'message':
|
|
890
|
+
turn.parts.start({ type: 'message', command: event.command, target: event.target });
|
|
891
|
+
return;
|
|
892
|
+
case 'marker':
|
|
893
|
+
turn.parts.start({ type: 'marker', marker: event.marker, text: event.text });
|
|
894
|
+
return;
|
|
895
|
+
case 'plan':
|
|
896
|
+
turn.parts.plan(event.entries);
|
|
897
|
+
return;
|
|
898
|
+
case 'image':
|
|
899
|
+
turn.uploads.push(this.image(conversation, turn, turn.parts.reserve(), event.data, event.mediaType));
|
|
900
|
+
this.kit.changed();
|
|
901
|
+
return;
|
|
585
902
|
}
|
|
586
|
-
turn.segment ??= { messageId, phase, text: '' };
|
|
587
|
-
turn.segment.text += text;
|
|
588
903
|
}
|
|
589
|
-
|
|
590
|
-
const
|
|
591
|
-
if (
|
|
904
|
+
subagentDone(conversation, id, result) {
|
|
905
|
+
const subagent = conversation.subagents.get(id);
|
|
906
|
+
if (subagent === undefined)
|
|
592
907
|
return;
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
note(conversation, turn, text) {
|
|
597
|
-
if (text.trim() === '')
|
|
908
|
+
conversation.subagents.delete(id);
|
|
909
|
+
if (subagent.turn === conversation.turn) {
|
|
910
|
+
subagent.turn.parts.done(subagent.index, result);
|
|
598
911
|
return;
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
912
|
+
}
|
|
913
|
+
const part = { type: 'subagent', description: subagent.description, status: 'done', result };
|
|
914
|
+
if (conversation.turn === null)
|
|
915
|
+
conversation.held.push(part);
|
|
602
916
|
else
|
|
603
|
-
turn.
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
917
|
+
conversation.turn.parts.start(part);
|
|
918
|
+
}
|
|
919
|
+
async image(conversation, turn, reserved, data, mediaType) {
|
|
920
|
+
const bytes = Buffer.from(data, 'base64');
|
|
921
|
+
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
922
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
923
|
+
try {
|
|
924
|
+
const declared = (await this.deliver(conversation, turn, 'images', {
|
|
925
|
+
name,
|
|
926
|
+
media_type: mediaType,
|
|
927
|
+
bytes: bytes.length,
|
|
928
|
+
sha256,
|
|
929
|
+
}));
|
|
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);
|
|
624
939
|
}
|
|
625
|
-
|
|
626
|
-
|
|
940
|
+
catch (error) {
|
|
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);
|
|
627
943
|
}
|
|
628
944
|
}
|
|
629
|
-
plan(conversation, turn) {
|
|
630
|
-
const sent = this.kit.send({
|
|
631
|
-
type: 'plan',
|
|
632
|
-
conversation_id: conversation.id,
|
|
633
|
-
turn_id: turn.id,
|
|
634
|
-
sequence: turn.planSequence + 1,
|
|
635
|
-
from: 0,
|
|
636
|
-
steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
|
|
637
|
-
});
|
|
638
|
-
if (sent)
|
|
639
|
-
turn.planSequence++;
|
|
640
|
-
}
|
|
641
945
|
}
|