@agentshouse/kit 0.2.1-alpha.9 → 0.2.1-c031c7457a661
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 +48 -29
- package/dist/abort.js +9 -0
- package/dist/acp.js +49 -86
- package/dist/agents.js +333 -52
- package/dist/api.js +40 -4
- package/dist/app-git.js +48 -0
- package/dist/bootstrap.json +10 -14
- package/dist/bridge.js +93 -78
- package/dist/calls.js +63 -0
- package/dist/chatgpt.js +32 -0
- package/dist/clis-main.js +23 -0
- package/dist/clis.js +217 -72
- package/dist/codex-main.js +58 -0
- package/dist/connection.js +126 -0
- package/dist/conversations.js +642 -263
- package/dist/copies.js +237 -173
- 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-main.js +2 -2
- package/dist/git.js +16 -10
- package/dist/home.js +127 -9
- package/dist/house-cli.js +300 -0
- package/dist/house-main.js +5 -102
- package/dist/instructions.js +16 -17
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -16
- package/dist/login.js +128 -95
- package/dist/logout.js +19 -0
- package/dist/mcp.js +41 -0
- package/dist/operation.js +7 -4
- package/dist/parts.js +82 -0
- package/dist/placement.js +39 -0
- package/dist/refusals.js +53 -0
- package/dist/relay.js +20 -0
- package/dist/resident.js +139 -52
- package/dist/scope.js +206 -0
- package/dist/secret-input.js +7 -2
- package/dist/service-main.js +9 -0
- package/dist/service.js +55 -0
- package/dist/shell.js +3 -0
- package/dist/sign-in.js +50 -31
- package/dist/skills.js +20 -37
- package/dist/transcription.js +201 -0
- package/dist/translate.js +2 -8
- package/dist/update.js +138 -0
- package/dist/web-wizard.js +100 -0
- package/dist/wire.js +22 -0
- package/dist/work.js +346 -0
- package/package.json +9 -11
- package/dist/blocks.js +0 -51
- package/dist/stream.js +0 -47
- package/how-we-work.md +0 -25
- 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,65 +1,152 @@
|
|
|
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
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { unlessAborted } from './abort.js';
|
|
6
|
+
import { startAdapter } from './acp.js';
|
|
7
|
+
import { finalRefusal, HouseRefusal } from './api.js';
|
|
7
8
|
import { openBridge } from './bridge.js';
|
|
8
9
|
import { CLIS } from './clis.js';
|
|
9
|
-
import {
|
|
10
|
+
import { answeredFailure, readerFor } from './events.js';
|
|
11
|
+
import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
|
|
12
|
+
import { agentBase } from './home.js';
|
|
10
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';
|
|
11
18
|
import { holdSecretInput } from './secret-input.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
19
|
+
import { transcribe } from './transcription.js';
|
|
20
|
+
import { workFor } from './work.js';
|
|
21
|
+
const SIGNED_OUT = RequestError.authRequired().code;
|
|
14
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']);
|
|
15
69
|
function causeOf(error) {
|
|
16
70
|
return error instanceof Error ? error.message : String(error);
|
|
17
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
|
+
}
|
|
18
78
|
function answerMessage(request, response) {
|
|
19
79
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
20
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
|
+
}
|
|
21
87
|
function logged(error) {
|
|
22
88
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
23
89
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
work.then(resolve, reject).finally(() => signal.removeEventListener('abort', killed));
|
|
31
|
-
});
|
|
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);
|
|
32
96
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
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}`;
|
|
35
105
|
}
|
|
36
106
|
class Turn {
|
|
37
107
|
id = randomUUID();
|
|
38
|
-
|
|
39
|
-
sent = [];
|
|
40
|
-
unsentFrom = null;
|
|
41
|
-
draftSequence = 0;
|
|
42
|
-
planSequence = 0;
|
|
43
|
-
entries = null;
|
|
108
|
+
parts;
|
|
44
109
|
questions = 0;
|
|
45
110
|
secrets = 0;
|
|
46
|
-
|
|
111
|
+
prompted = false;
|
|
112
|
+
taken = null;
|
|
47
113
|
ended = false;
|
|
114
|
+
images = 0;
|
|
115
|
+
uploads = [];
|
|
116
|
+
constructor(emit) {
|
|
117
|
+
this.parts = new Parts(emit);
|
|
118
|
+
}
|
|
48
119
|
}
|
|
49
120
|
class Conversation {
|
|
50
121
|
id;
|
|
51
122
|
running = null;
|
|
123
|
+
session = null;
|
|
52
124
|
turn = null;
|
|
53
125
|
commands = null;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
126
|
+
options = null;
|
|
127
|
+
exitUntold = false;
|
|
128
|
+
context = null;
|
|
129
|
+
leftovers = new Set();
|
|
130
|
+
waiting = [];
|
|
131
|
+
sending = null;
|
|
132
|
+
subagents = new Map();
|
|
133
|
+
held = [];
|
|
57
134
|
opening = null;
|
|
135
|
+
bridge = null;
|
|
136
|
+
launch = null;
|
|
137
|
+
prompted = null;
|
|
58
138
|
kills = 0;
|
|
59
139
|
killed = new AbortController();
|
|
140
|
+
interrupts = 0;
|
|
141
|
+
interrupted = new AbortController();
|
|
60
142
|
open = 0;
|
|
61
143
|
queue = Promise.resolve();
|
|
62
144
|
reports = Promise.resolve();
|
|
145
|
+
starting = null;
|
|
146
|
+
preparing = null;
|
|
147
|
+
told = null;
|
|
148
|
+
untold = false;
|
|
149
|
+
shown = null;
|
|
63
150
|
constructor(id) {
|
|
64
151
|
this.id = id;
|
|
65
152
|
}
|
|
@@ -75,51 +162,80 @@ function asksSecret(request) {
|
|
|
75
162
|
export class Conversations {
|
|
76
163
|
kit;
|
|
77
164
|
conversations = new Map();
|
|
78
|
-
acks = new Map();
|
|
79
165
|
questions = new Map();
|
|
80
|
-
carrying = 0;
|
|
81
166
|
reporting = 0;
|
|
167
|
+
watched = false;
|
|
82
168
|
constructor(kit) {
|
|
83
169
|
this.kit = kit;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.then((
|
|
102
|
-
.catch(
|
|
103
|
-
.
|
|
104
|
-
this.carrying--;
|
|
105
|
-
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 }));
|
|
106
190
|
});
|
|
107
191
|
}
|
|
108
192
|
idle() {
|
|
109
|
-
if (this.
|
|
193
|
+
if (this.reporting > 0)
|
|
110
194
|
return false;
|
|
111
195
|
for (const conversation of this.conversations.values()) {
|
|
112
196
|
const turn = conversation.turn;
|
|
113
197
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
114
|
-
if (conversation.
|
|
198
|
+
if (conversation.open > (asking ? 1 : 0))
|
|
115
199
|
return false;
|
|
116
200
|
}
|
|
117
|
-
|
|
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();
|
|
118
219
|
}
|
|
119
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;
|
|
120
236
|
for (const conversation of this.conversations.values()) {
|
|
121
237
|
if (conversation.turn !== null)
|
|
122
|
-
conversation.turn
|
|
238
|
+
this.replay(conversation, conversation.turn);
|
|
123
239
|
}
|
|
124
240
|
}
|
|
125
241
|
conversation(id) {
|
|
@@ -130,69 +246,249 @@ export class Conversations {
|
|
|
130
246
|
}
|
|
131
247
|
return conversation;
|
|
132
248
|
}
|
|
133
|
-
async carry(conversation,
|
|
134
|
-
if (
|
|
135
|
-
return this.open(conversation,
|
|
136
|
-
if (
|
|
137
|
-
return this.message(conversation,
|
|
138
|
-
|
|
139
|
-
|
|
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') {
|
|
256
|
+
if (--conversation.interrupts === 0)
|
|
257
|
+
conversation.interrupted = new AbortController();
|
|
258
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
259
|
+
}
|
|
260
|
+
if (call.kind === 'kill') {
|
|
141
261
|
await this.kill(conversation);
|
|
262
|
+
await conversation.sending;
|
|
142
263
|
if (--conversation.kills === 0)
|
|
143
264
|
conversation.killed = new AbortController();
|
|
144
265
|
}
|
|
145
|
-
if (
|
|
146
|
-
await this.option(conversation, String(
|
|
147
|
-
if (
|
|
148
|
-
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));
|
|
149
270
|
if (question === undefined) {
|
|
150
|
-
|
|
271
|
+
const text = answerMessage(call.request, call.response);
|
|
272
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
151
273
|
}
|
|
152
|
-
question.answer(
|
|
274
|
+
question.answer(call.response);
|
|
153
275
|
}
|
|
154
|
-
return {};
|
|
276
|
+
return { result: Promise.resolve({}) };
|
|
155
277
|
}
|
|
156
|
-
|
|
278
|
+
open(conversation, call) {
|
|
157
279
|
if (conversation.running !== null)
|
|
158
|
-
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;
|
|
159
324
|
try {
|
|
160
|
-
|
|
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);
|
|
161
332
|
}
|
|
162
333
|
catch (error) {
|
|
163
|
-
|
|
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;
|
|
164
355
|
}
|
|
356
|
+
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
357
|
+
conversation.sending = null;
|
|
358
|
+
this.next(conversation);
|
|
359
|
+
});
|
|
165
360
|
}
|
|
166
|
-
async
|
|
167
|
-
|
|
361
|
+
async send(conversation, waiting) {
|
|
362
|
+
const { call } = waiting;
|
|
363
|
+
let result = {};
|
|
364
|
+
let started = false;
|
|
365
|
+
let instructing = waiting.instructing;
|
|
168
366
|
const prompt = [];
|
|
169
367
|
try {
|
|
170
|
-
const route = await this.route(conversation, input.agent_id);
|
|
171
|
-
const paths = await placeFiles(this.kit.house, route.working_directory, input.files, conversation.killed.signal);
|
|
172
368
|
if (conversation.running === null) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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 };
|
|
176
383
|
}
|
|
177
|
-
if (
|
|
178
|
-
prompt.push({
|
|
179
|
-
type: 'text',
|
|
180
|
-
text: await instructions(conversation.running.bridge, route.base_instructions, await this.kit.copies.mapping()),
|
|
181
|
-
});
|
|
384
|
+
if (call.first === true) {
|
|
385
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
182
386
|
}
|
|
183
|
-
prompt.push({ type: 'text', text: String(input.text) });
|
|
184
|
-
if (paths.length > 0)
|
|
185
|
-
prompt.push({ type: 'text', text: paths.join('\n') });
|
|
186
387
|
if (conversation.kills > 0)
|
|
187
388
|
throw new Error(KILLED);
|
|
188
389
|
}
|
|
189
390
|
catch (error) {
|
|
190
|
-
|
|
391
|
+
this.starts(conversation, null);
|
|
392
|
+
if (started) {
|
|
393
|
+
conversation.session = null;
|
|
191
394
|
await this.kill(conversation);
|
|
192
|
-
|
|
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;
|
|
413
|
+
}
|
|
414
|
+
conversation.shown = setTimeout(() => {
|
|
415
|
+
conversation.shown = null;
|
|
416
|
+
if (conversation.starting === step)
|
|
417
|
+
this.tell(conversation, step);
|
|
418
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
419
|
+
}
|
|
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}` });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
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
|
+
});
|
|
193
478
|
}
|
|
194
|
-
|
|
195
|
-
|
|
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);
|
|
196
492
|
}
|
|
197
493
|
async interrupt(conversation, turnId) {
|
|
198
494
|
if (conversation.running === null || conversation.turn?.id !== turnId)
|
|
@@ -203,11 +499,13 @@ export class Conversations {
|
|
|
203
499
|
}
|
|
204
500
|
halt(conversation) {
|
|
205
501
|
if (conversation.opening !== null)
|
|
206
|
-
|
|
502
|
+
killMarked(marker(conversation.opening));
|
|
503
|
+
for (const scope of conversation.leftovers)
|
|
504
|
+
scope.kill();
|
|
207
505
|
if (conversation.running === null)
|
|
208
506
|
return;
|
|
209
507
|
conversation.running.killed = true;
|
|
210
|
-
|
|
508
|
+
conversation.running.scope.kill();
|
|
211
509
|
}
|
|
212
510
|
async kill(conversation) {
|
|
213
511
|
const running = conversation.running;
|
|
@@ -219,6 +517,7 @@ export class Conversations {
|
|
|
219
517
|
if (running === null)
|
|
220
518
|
return;
|
|
221
519
|
try {
|
|
520
|
+
running.reader.requested(option, value);
|
|
222
521
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
223
522
|
sessionId: running.sessionId,
|
|
224
523
|
configId: option,
|
|
@@ -227,73 +526,98 @@ export class Conversations {
|
|
|
227
526
|
this.options(conversation, answered.configOptions);
|
|
228
527
|
}
|
|
229
528
|
catch (error) {
|
|
529
|
+
running.reader.refused(option);
|
|
230
530
|
logged(error);
|
|
231
531
|
}
|
|
232
532
|
}
|
|
233
533
|
async route(conversation, agentId) {
|
|
234
534
|
const signal = conversation.killed.signal;
|
|
235
|
-
await
|
|
535
|
+
await unlessAborted(this.kit.agents.read(), signal);
|
|
236
536
|
if (this.kit.agents.route(agentId) === undefined)
|
|
237
|
-
await
|
|
537
|
+
await unlessAborted(this.kit.agents.refresh(), signal);
|
|
238
538
|
const route = this.kit.agents.route(agentId);
|
|
239
539
|
if (route === undefined)
|
|
240
540
|
throw new Error(`this Environment hosts no Agent ${agentId}`);
|
|
241
|
-
if (route.working_directory === join(
|
|
541
|
+
if (route.working_directory === join(agentBase(), route.agent_id)) {
|
|
242
542
|
await mkdir(route.working_directory, { recursive: true });
|
|
243
543
|
}
|
|
244
544
|
return route;
|
|
245
545
|
}
|
|
246
|
-
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;
|
|
247
550
|
const app = client({ name: '@agentshouse/kit' })
|
|
248
|
-
.onNotification('session/update', ({ params }) => this.update(conversation, params))
|
|
249
|
-
.onNotification(TURN_STARTED, (params) => params, () => void this.started(conversation))
|
|
250
|
-
.onNotification(TURN_ENDED, (params) => params, () => this.ended(conversation))
|
|
251
551
|
.onRequest('session/request_permission', ({ params, signal }) => this.ask(conversation, 'session/request_permission', params, false, signal))
|
|
252
|
-
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
253
|
-
|
|
254
|
-
const
|
|
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));
|
|
554
|
+
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
555
|
+
const bridging = this.bridged(conversation);
|
|
556
|
+
const bridge = await step(unbridged, bridging);
|
|
255
557
|
if (conversation.kills > 0) {
|
|
256
|
-
|
|
558
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
257
559
|
throw new Error(KILLED);
|
|
258
560
|
}
|
|
259
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;
|
|
260
565
|
try {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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));
|
|
264
574
|
const agent = adapter.connection.agent;
|
|
265
575
|
const cwd = route.working_directory;
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
sessionId: opened.sessionId,
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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());
|
|
277
587
|
conversation.running = running;
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
this.
|
|
282
|
-
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));
|
|
283
592
|
return running.sessionId;
|
|
284
593
|
}
|
|
285
594
|
catch (error) {
|
|
286
|
-
|
|
287
|
-
|
|
595
|
+
adapter?.retire();
|
|
596
|
+
work.close();
|
|
597
|
+
conversation.leftovers.add(scope);
|
|
598
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
288
599
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
289
600
|
}
|
|
290
601
|
finally {
|
|
291
602
|
conversation.opening = null;
|
|
292
603
|
}
|
|
293
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
|
+
}
|
|
294
617
|
async launchSettings(running, route, offered) {
|
|
295
618
|
let options = offered;
|
|
296
619
|
const settings = [
|
|
620
|
+
['mode', route.mode ?? CLIS[route.kind].fullAccess],
|
|
297
621
|
['model', route.model],
|
|
298
622
|
['thought_level', route.effort],
|
|
299
623
|
];
|
|
@@ -301,6 +625,7 @@ export class Conversations {
|
|
|
301
625
|
if (value === null)
|
|
302
626
|
continue;
|
|
303
627
|
const option = options.find((candidate) => candidate.category === category);
|
|
628
|
+
running.reader.requested(option?.id ?? category, value);
|
|
304
629
|
const answered = await running.adapter.connection.agent.request('session/set_config_option', {
|
|
305
630
|
sessionId: running.sessionId,
|
|
306
631
|
configId: option?.id ?? category,
|
|
@@ -310,23 +635,37 @@ export class Conversations {
|
|
|
310
635
|
}
|
|
311
636
|
return options;
|
|
312
637
|
}
|
|
313
|
-
exited(conversation,
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
|
|
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);
|
|
317
651
|
conversation.running = null;
|
|
318
652
|
conversation.commands = null;
|
|
319
|
-
conversation.
|
|
320
|
-
conversation.queued.length = 0;
|
|
653
|
+
conversation.options = null;
|
|
321
654
|
for (const [id, question] of this.questions) {
|
|
322
655
|
if (question.conversation === conversation)
|
|
323
656
|
this.questions.delete(id);
|
|
324
657
|
}
|
|
325
|
-
this.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
|
|
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);
|
|
330
669
|
}
|
|
331
670
|
commands(conversation, commands) {
|
|
332
671
|
conversation.commands = commands;
|
|
@@ -337,11 +676,12 @@ export class Conversations {
|
|
|
337
676
|
options(conversation, options) {
|
|
338
677
|
if (conversation.running === null)
|
|
339
678
|
return;
|
|
679
|
+
conversation.options = options;
|
|
340
680
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
341
681
|
}
|
|
342
682
|
deliver(conversation, turn, event, body) {
|
|
343
683
|
this.reporting++;
|
|
344
|
-
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));
|
|
345
685
|
conversation.reports = delivered
|
|
346
686
|
.catch(() => undefined)
|
|
347
687
|
.finally(() => {
|
|
@@ -350,105 +690,95 @@ export class Conversations {
|
|
|
350
690
|
});
|
|
351
691
|
return delivered;
|
|
352
692
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
.then(() => this.admitted(conversation, turn))
|
|
358
|
-
.catch(logged);
|
|
359
|
-
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 });
|
|
360
697
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
this.draft(conversation, turn);
|
|
365
|
-
}
|
|
366
|
-
if (turn.entries !== null)
|
|
367
|
-
this.plan(conversation, turn);
|
|
698
|
+
replay(conversation, turn) {
|
|
699
|
+
for (const operation of turn.parts.operations())
|
|
700
|
+
this.write(conversation, turn, operation);
|
|
368
701
|
}
|
|
369
702
|
begin(conversation) {
|
|
370
|
-
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);
|
|
371
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);
|
|
372
711
|
this.kit.changed();
|
|
373
712
|
return turn;
|
|
374
713
|
}
|
|
375
|
-
end(conversation, turn,
|
|
714
|
+
end(conversation, turn, failed, notice = null) {
|
|
376
715
|
if (turn.ended)
|
|
377
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 });
|
|
378
725
|
turn.ended = true;
|
|
379
726
|
conversation.open--;
|
|
380
727
|
if (conversation.turn === turn)
|
|
381
728
|
conversation.turn = null;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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);
|
|
386
736
|
this.kit.changed();
|
|
387
737
|
}
|
|
388
|
-
|
|
389
|
-
if (conversation.open > 0)
|
|
390
|
-
conversation.late.push(cause);
|
|
391
|
-
else
|
|
392
|
-
this.end(conversation, this.report(conversation), { failed: cause });
|
|
393
|
-
}
|
|
394
|
-
prompt(conversation, prompt) {
|
|
738
|
+
prompt(conversation, prompt, taken) {
|
|
395
739
|
const running = conversation.running;
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
this.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
else
|
|
407
|
-
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));
|
|
408
764
|
}
|
|
409
765
|
});
|
|
410
766
|
}
|
|
411
|
-
attach(turn, sent) {
|
|
412
|
-
turn.prompt = sent;
|
|
413
|
-
sent.turn = turn;
|
|
414
|
-
}
|
|
415
|
-
answered(conversation, sent, failed) {
|
|
416
|
-
const turn = sent.turn;
|
|
417
|
-
if (turn === null) {
|
|
418
|
-
const queued = conversation.queued.indexOf(sent);
|
|
419
|
-
if (queued < 0)
|
|
420
|
-
return;
|
|
421
|
-
conversation.queued.splice(queued, 1);
|
|
422
|
-
if (failed !== null)
|
|
423
|
-
this.failed(conversation, failed);
|
|
424
|
-
}
|
|
425
|
-
else if (failed !== null) {
|
|
426
|
-
if (turn.ended)
|
|
427
|
-
this.failed(conversation, failed);
|
|
428
|
-
else
|
|
429
|
-
this.end(conversation, turn, { failed });
|
|
430
|
-
}
|
|
431
|
-
else if (turn.prompt === sent) {
|
|
432
|
-
this.end(conversation, turn, { text: turn.text });
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
767
|
started(conversation) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const turn = this.begin(conversation);
|
|
439
|
-
const queued = conversation.queued.shift();
|
|
440
|
-
if (queued !== undefined)
|
|
441
|
-
this.attach(turn, queued);
|
|
768
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
769
|
+
this.took(turn);
|
|
442
770
|
return turn;
|
|
443
771
|
}
|
|
444
772
|
ended(conversation) {
|
|
445
773
|
const turn = conversation.turn;
|
|
446
774
|
if (turn === null)
|
|
447
775
|
return;
|
|
448
|
-
if (turn.
|
|
449
|
-
this.end(conversation, turn,
|
|
776
|
+
if (!turn.prompted) {
|
|
777
|
+
this.end(conversation, turn, null);
|
|
450
778
|
}
|
|
451
779
|
else {
|
|
780
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
781
|
+
this.apply(conversation, turn, event);
|
|
452
782
|
conversation.turn = null;
|
|
453
783
|
this.kit.changed();
|
|
454
784
|
}
|
|
@@ -497,65 +827,114 @@ export class Conversations {
|
|
|
497
827
|
if (content !== null)
|
|
498
828
|
this.questions.get(interactionId)?.answer({ action: 'accept', content });
|
|
499
829
|
}
|
|
500
|
-
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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;
|
|
517
862
|
}
|
|
518
|
-
|
|
519
|
-
|
|
863
|
+
this.apply(conversation, this.started(conversation), event);
|
|
864
|
+
}
|
|
865
|
+
took(turn) {
|
|
866
|
+
const taken = turn.taken;
|
|
867
|
+
turn.taken = null;
|
|
868
|
+
taken?.();
|
|
869
|
+
}
|
|
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;
|
|
520
897
|
}
|
|
521
898
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
899
|
+
subagentDone(conversation, id, result) {
|
|
900
|
+
const subagent = conversation.subagents.get(id);
|
|
901
|
+
if (subagent === undefined)
|
|
902
|
+
return;
|
|
903
|
+
conversation.subagents.delete(id);
|
|
904
|
+
if (subagent.turn === conversation.turn) {
|
|
905
|
+
subagent.turn.parts.done(subagent.index, result);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const part = { type: 'subagent', description: subagent.description, status: 'done', result };
|
|
909
|
+
if (conversation.turn === null)
|
|
910
|
+
conversation.held.push(part);
|
|
525
911
|
else
|
|
526
|
-
conversation.
|
|
527
|
-
this.kit.changed();
|
|
912
|
+
conversation.turn.parts.start(part);
|
|
528
913
|
}
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
const
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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);
|
|
544
934
|
}
|
|
545
|
-
|
|
546
|
-
|
|
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);
|
|
547
938
|
}
|
|
548
939
|
}
|
|
549
|
-
plan(conversation, turn) {
|
|
550
|
-
const sent = this.kit.send({
|
|
551
|
-
type: 'plan',
|
|
552
|
-
conversation_id: conversation.id,
|
|
553
|
-
turn_id: turn.id,
|
|
554
|
-
sequence: turn.planSequence + 1,
|
|
555
|
-
from: 0,
|
|
556
|
-
steps: turn.entries.map((entry) => ({ label: entry.content, priority: entry.priority, status: entry.status })),
|
|
557
|
-
});
|
|
558
|
-
if (sent)
|
|
559
|
-
turn.planSequence++;
|
|
560
|
-
}
|
|
561
940
|
}
|