@agentshouse/kit 0.2.1-ca18b01951c3b → 0.2.1-cab5821cf0a77
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 +33 -33
- package/dist/acp.js +90 -19
- package/dist/agents.js +101 -45
- package/dist/api.js +5 -5
- package/dist/app-git.js +10 -26
- package/dist/authority-main.js +2 -2
- package/dist/bridge.js +4 -7
- package/dist/building.js +50 -0
- package/dist/calls.js +71 -0
- package/dist/chatgpt.js +4 -1
- package/dist/clis.js +43 -40
- package/dist/{stream.js → connection.js} +34 -52
- package/dist/conversations.js +516 -175
- package/dist/copies.js +77 -50
- package/dist/end.js +15 -4
- package/dist/events.js +33 -8
- package/dist/files.js +139 -37
- package/dist/git.js +5 -6
- package/dist/home.js +27 -20
- package/dist/house-cli.js +299 -0
- package/dist/house-main.js +7 -289
- package/dist/instructions.js +29 -8
- package/dist/kit-cli.js +68 -0
- package/dist/kit-main.js +8 -67
- package/dist/login.js +18 -9
- package/dist/logout.js +0 -2
- package/dist/mcp.js +6 -2
- package/dist/placement.js +19 -36
- package/dist/relay.js +21 -0
- package/dist/resident.js +97 -42
- package/dist/scope.js +15 -4
- package/dist/scripts.js +255 -0
- package/dist/service.js +23 -1
- package/dist/sign-in.js +22 -39
- package/dist/silence.js +28 -0
- package/dist/skills.js +42 -97
- package/dist/transcription.js +83 -43
- package/dist/translate.js +2 -8
- package/dist/update.js +113 -127
- package/dist/wire.js +37 -3
- package/dist/work.js +3 -5
- package/package.json +20 -21
- package/dist/bootstrap.json +0 -24
- package/dist/clis-main.js +0 -23
- package/how-we-work.md +0 -33
- package/skills/ask-matt/LICENSE +0 -21
- package/skills/ask-matt/PHASE-BOUNDARIES.md +0 -55
- package/skills/ask-matt/SKILL.md +0 -97
- package/skills/ask-matt/agents/openai.yaml +0 -5
- 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 -114
- package/skills/codebase-design/agents/openai.yaml +0 -3
- package/skills/diagnosing-bugs/LICENSE +0 -21
- package/skills/diagnosing-bugs/SKILL.md +0 -138
- 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/GLOSSARY-FORMAT.md +0 -60
- package/skills/domain-modeling/LICENSE +0 -21
- package/skills/domain-modeling/SKILL.md +0 -74
- package/skills/domain-modeling/agents/openai.yaml +0 -3
- package/skills/grill-me/LICENSE +0 -21
- package/skills/grill-me/SKILL.md +0 -7
- package/skills/grill-me/agents/openai.yaml +0 -5
- package/skills/grill-with-docs/LICENSE +0 -21
- package/skills/grill-with-docs/SKILL.md +0 -7
- package/skills/grill-with-docs/agents/openai.yaml +0 -5
- 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 -16
- package/skills/handoff/agents/openai.yaml +0 -5
- package/skills/implement/LICENSE +0 -21
- package/skills/implement/SKILL.md +0 -17
- package/skills/implement/agents/openai.yaml +0 -5
- package/skills/implement-spec/LICENSE +0 -21
- package/skills/implement-spec/SKILL.md +0 -40
- package/skills/implement-spec/agents/openai.yaml +0 -5
- 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 -71
- package/skills/improve-codebase-architecture/agents/openai.yaml +0 -5
- package/skills/pr/CREDITS.md +0 -3
- package/skills/pr/LICENSE +0 -21
- package/skills/pr/SKILL.md +0 -170
- package/skills/pr/agents/openai.yaml +0 -3
- package/skills/prototype/LICENSE +0 -21
- package/skills/prototype/LOGIC.md +0 -67
- package/skills/prototype/SKILL.md +0 -26
- 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 -12
- package/skills/research/agents/openai.yaml +0 -3
- package/skills/retro/LICENSE +0 -21
- package/skills/retro/SKILL.md +0 -44
- package/skills/retro/agents/openai.yaml +0 -5
- package/skills/tdd/LICENSE +0 -21
- package/skills/tdd/SKILL.md +0 -38
- 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 -140
- package/skills/teach/agents/openai.yaml +0 -5
- package/skills/to-questionnaire/LICENSE +0 -21
- package/skills/to-questionnaire/SKILL.md +0 -54
- package/skills/to-questionnaire/agents/openai.yaml +0 -5
- package/skills/to-spec/LICENSE +0 -21
- package/skills/to-spec/SKILL.md +0 -75
- package/skills/to-spec/agents/openai.yaml +0 -5
- package/skills/to-tickets/LICENSE +0 -21
- package/skills/to-tickets/SKILL.md +0 -105
- package/skills/to-tickets/agents/openai.yaml +0 -5
- package/skills/triage/AGENT-BRIEF.md +0 -207
- package/skills/triage/LICENSE +0 -21
- package/skills/triage/OUT-OF-SCOPE.md +0 -105
- package/skills/triage/SKILL.md +0 -112
- package/skills/triage/agents/openai.yaml +0 -5
- package/skills/wait-what/LICENSE +0 -21
- package/skills/wait-what/SKILL.md +0 -7
- package/skills/wait-what/agents/openai.yaml +0 -5
- package/skills/wayfinder/LICENSE +0 -21
- package/skills/wayfinder/SKILL.md +0 -128
- package/skills/wayfinder/agents/openai.yaml +0 -5
- package/skills/wizard/LICENSE +0 -21
- package/skills/wizard/SKILL.md +0 -44
- package/skills/wizard/agents/openai.yaml +0 -3
- package/skills/wizard/template.sh +0 -223
- 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 -81
- package/skills/writing-for-agents/agents/openai.yaml +0 -3
package/dist/conversations.js
CHANGED
|
@@ -3,20 +3,57 @@ 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 { startAdapter } from './acp.js';
|
|
6
|
+
import { signedOut, startAdapter } from './acp.js';
|
|
7
|
+
import { finalRefusal, HouseRefusal } from './api.js';
|
|
7
8
|
import { openBridge } from './bridge.js';
|
|
8
9
|
import { CLIS } from './clis.js';
|
|
9
|
-
import { readerFor } from './events.js';
|
|
10
|
-
import {
|
|
10
|
+
import { answeredFailure, readerFor } from './events.js';
|
|
11
|
+
import { inspectedFile, linkedPaths, storeRefusal, uploaded } from './files.js';
|
|
11
12
|
import { agentBase } from './home.js';
|
|
12
13
|
import { instructions } from './instructions.js';
|
|
13
14
|
import { Parts } from './parts.js';
|
|
15
|
+
import { refusalOf } from './refusals.js';
|
|
16
|
+
import { relayed } from './relay.js';
|
|
14
17
|
import { killMarked, Scope } from './scope.js';
|
|
15
18
|
import { holdSecretInput } from './secret-input.js';
|
|
16
|
-
import { createHowWeWork } from './skills.js';
|
|
17
19
|
import { transcribe } from './transcription.js';
|
|
18
20
|
import { workFor } from './work.js';
|
|
19
21
|
const KILLED = 'the conversation was killed';
|
|
22
|
+
const INTERRUPTED = 'the conversation was interrupted';
|
|
23
|
+
const REFUSED = 'the CLI refused the prompt';
|
|
24
|
+
const NO_CONNECTION = "the Agent's computer could not open this conversation; try again";
|
|
25
|
+
const GONE = 'House no longer holds this conversation; start a new one';
|
|
26
|
+
const RETIRED = "this conversation's Agent is retired, so it cannot start";
|
|
27
|
+
const ENDED = {
|
|
28
|
+
conversation_not_found: GONE,
|
|
29
|
+
conversation_historical: RETIRED,
|
|
30
|
+
};
|
|
31
|
+
const NO_START = "the Agent's CLI did not start";
|
|
32
|
+
const NO_SESSION = "the Agent's CLI did not open a session";
|
|
33
|
+
const NO_RESUME = "the Agent's CLI could not resume this conversation";
|
|
34
|
+
const NOT_SIGNED_IN = "the Agent's CLI is not signed in; sign it in on the Agent's machine";
|
|
35
|
+
const NO_SETTINGS = "the Agent's CLI refused the Agent's model, mode or effort";
|
|
36
|
+
const UNSTEERED = "the Agent's CLI did not take the message into its running turn";
|
|
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 unopened(reason) {
|
|
50
|
+
return (error) => (signedOut(error) ? NOT_SIGNED_IN : reason);
|
|
51
|
+
}
|
|
52
|
+
// A start-up step is shown only once it has lasted a second, so a quick start shows none of them.
|
|
53
|
+
const STARTING_SHOWN_AFTER_MS = 1_000;
|
|
54
|
+
function unbridged(error) {
|
|
55
|
+
return (finalRefusal(error) ? ENDED[refusalOf(error.text)?.code ?? ''] : undefined) ?? NO_CONNECTION;
|
|
56
|
+
}
|
|
20
57
|
const EXIT_PLAN_MODE = {
|
|
21
58
|
parse(value) {
|
|
22
59
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
@@ -25,26 +62,57 @@ const EXIT_PLAN_MODE = {
|
|
|
25
62
|
return value;
|
|
26
63
|
},
|
|
27
64
|
};
|
|
28
|
-
export const
|
|
65
|
+
export const CONVERSATION_METHODS = new Set(['open', 'message', 'interrupt', 'kill', 'done', 'option', 'answer']);
|
|
29
66
|
function causeOf(error) {
|
|
30
67
|
return error instanceof Error ? error.message : String(error);
|
|
31
68
|
}
|
|
69
|
+
function answeredWith(error) {
|
|
70
|
+
return { failure: signedOut(error) ? 'signed_out' : 'error', cause: causeOf(error) };
|
|
71
|
+
}
|
|
72
|
+
function exitedWith(running, cause) {
|
|
73
|
+
if (running.killed)
|
|
74
|
+
return { failure: 'killed', cause: KILLED };
|
|
75
|
+
return { failure: running.adapter.silent() ? 'silent' : 'exited', cause };
|
|
76
|
+
}
|
|
32
77
|
function answerMessage(request, response) {
|
|
33
78
|
return `The User answered a question you asked earlier.\nQuestion: ${JSON.stringify(request)}\nAnswer: ${JSON.stringify(response)}`;
|
|
34
79
|
}
|
|
80
|
+
async function sentPart(url, bytes, headers) {
|
|
81
|
+
const answer = await fetch(relayed(url), { method: 'PUT', headers, body: bytes });
|
|
82
|
+
await answer.arrayBuffer();
|
|
83
|
+
if (!answer.ok)
|
|
84
|
+
throw storeRefusal('upload', answer.status);
|
|
85
|
+
}
|
|
35
86
|
function logged(error) {
|
|
36
87
|
process.stderr.write(`kit: ${causeOf(error)}\n`);
|
|
37
88
|
}
|
|
89
|
+
function refusal(conversation, error) {
|
|
90
|
+
if (conversation.kills > 0)
|
|
91
|
+
return KILLED;
|
|
92
|
+
const cause = error instanceof Error && error.cause !== undefined ? `${error.message}: ${causeOf(error.cause)}` : causeOf(error);
|
|
93
|
+
logged(new Error(`conversation ${conversation.id} did not start: ${cause}`));
|
|
94
|
+
return causeOf(error);
|
|
95
|
+
}
|
|
96
|
+
function audioOf(result) {
|
|
97
|
+
if (result.isError === true)
|
|
98
|
+
return null;
|
|
99
|
+
const file = inspectedFile(result.content.map((content) => content.text).join('\n'));
|
|
100
|
+
return file?.media_type?.startsWith('audio/') === true ? file : null;
|
|
101
|
+
}
|
|
38
102
|
function marker(bridge) {
|
|
39
103
|
return `HOUSE_BRIDGE=${bridge.env.HOUSE_BRIDGE}`;
|
|
40
104
|
}
|
|
105
|
+
function steers(adapter) {
|
|
106
|
+
return adapter.initialized._meta?.steering?.supported === true;
|
|
107
|
+
}
|
|
41
108
|
class Turn {
|
|
42
109
|
id = randomUUID();
|
|
43
110
|
parts;
|
|
44
|
-
admitted = false;
|
|
45
111
|
questions = 0;
|
|
46
112
|
secrets = 0;
|
|
47
113
|
prompted = false;
|
|
114
|
+
steered = false;
|
|
115
|
+
taken = null;
|
|
48
116
|
ended = false;
|
|
49
117
|
images = 0;
|
|
50
118
|
uploads = [];
|
|
@@ -58,20 +126,31 @@ class Conversation {
|
|
|
58
126
|
session = null;
|
|
59
127
|
turn = null;
|
|
60
128
|
commands = null;
|
|
129
|
+
options = null;
|
|
130
|
+
exitUntold = false;
|
|
61
131
|
context = null;
|
|
62
132
|
leftovers = new Set();
|
|
63
133
|
waiting = [];
|
|
134
|
+
done = false;
|
|
64
135
|
sending = null;
|
|
65
136
|
subagents = new Map();
|
|
66
137
|
held = [];
|
|
67
138
|
opening = null;
|
|
139
|
+
bridge = null;
|
|
140
|
+
launch = null;
|
|
141
|
+
prompted = null;
|
|
68
142
|
kills = 0;
|
|
69
143
|
killed = new AbortController();
|
|
70
144
|
interrupts = 0;
|
|
71
145
|
interrupted = new AbortController();
|
|
72
|
-
|
|
146
|
+
turns = new Set();
|
|
73
147
|
queue = Promise.resolve();
|
|
74
148
|
reports = Promise.resolve();
|
|
149
|
+
starting = null;
|
|
150
|
+
preparing = null;
|
|
151
|
+
told = null;
|
|
152
|
+
untold = false;
|
|
153
|
+
shown = null;
|
|
75
154
|
constructor(id) {
|
|
76
155
|
this.id = id;
|
|
77
156
|
}
|
|
@@ -87,9 +166,7 @@ function asksSecret(request) {
|
|
|
87
166
|
export class Conversations {
|
|
88
167
|
kit;
|
|
89
168
|
conversations = new Map();
|
|
90
|
-
acks = new Map();
|
|
91
169
|
questions = new Map();
|
|
92
|
-
carrying = 0;
|
|
93
170
|
reporting = 0;
|
|
94
171
|
watched = false;
|
|
95
172
|
constructor(kit) {
|
|
@@ -97,43 +174,32 @@ export class Conversations {
|
|
|
97
174
|
// A process or a durable schedule ends without a report, so the Kit looks at both again every two seconds.
|
|
98
175
|
setInterval(() => this.look(), 2_000);
|
|
99
176
|
}
|
|
100
|
-
|
|
101
|
-
this.
|
|
102
|
-
|
|
103
|
-
(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
conversation.queue = handled.catch(() => undefined);
|
|
118
|
-
const work = handled.then(({ ack }) => ack);
|
|
119
|
-
this.acks.set(input.input_id, work);
|
|
120
|
-
return work;
|
|
121
|
-
})();
|
|
122
|
-
carried
|
|
123
|
-
.then((body) => this.kit.house.deliver(`/kit/inputs/${input.input_id}/ack`, body))
|
|
124
|
-
.catch(logged)
|
|
125
|
-
.finally(() => {
|
|
126
|
-
this.carrying--;
|
|
127
|
-
this.kit.changed();
|
|
177
|
+
call(call) {
|
|
178
|
+
void this.kit.calls.carry(call.input_id, () => {
|
|
179
|
+
const conversation = this.conversation(call.conversation_id);
|
|
180
|
+
if (call.kind === 'kill') {
|
|
181
|
+
conversation.kills++;
|
|
182
|
+
conversation.killed.abort(new Error(KILLED));
|
|
183
|
+
for (const waiting of conversation.waiting.splice(0))
|
|
184
|
+
waiting.settle({ refused: KILLED });
|
|
185
|
+
this.halt(conversation);
|
|
186
|
+
}
|
|
187
|
+
if (call.kind === 'interrupt') {
|
|
188
|
+
conversation.interrupts++;
|
|
189
|
+
conversation.interrupted.abort();
|
|
190
|
+
}
|
|
191
|
+
const handled = conversation.queue.then(() => this.carry(conversation, call));
|
|
192
|
+
conversation.queue = handled.catch(() => undefined);
|
|
193
|
+
return handled.then(async ({ result }) => ({ result: await result }));
|
|
128
194
|
});
|
|
129
195
|
}
|
|
130
196
|
idle() {
|
|
131
|
-
if (this.
|
|
197
|
+
if (this.reporting > 0)
|
|
132
198
|
return false;
|
|
133
199
|
for (const conversation of this.conversations.values()) {
|
|
134
200
|
const turn = conversation.turn;
|
|
135
201
|
const asking = turn !== null && turn.questions > 0 && turn.secrets === 0;
|
|
136
|
-
if (conversation.
|
|
202
|
+
if (conversation.turns.size > (asking ? 1 : 0))
|
|
137
203
|
return false;
|
|
138
204
|
}
|
|
139
205
|
const conversations = [...this.conversations.values()];
|
|
@@ -142,7 +208,7 @@ export class Conversations {
|
|
|
142
208
|
return !conversations.some((conversation) => conversation.running?.scope.working() || [...conversation.leftovers].some((scope) => scope.working()));
|
|
143
209
|
}
|
|
144
210
|
quiet() {
|
|
145
|
-
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.
|
|
211
|
+
return this.idle() && [...this.conversations.values()].every((conversation) => conversation.turns.size === 0);
|
|
146
212
|
}
|
|
147
213
|
look() {
|
|
148
214
|
for (const conversation of this.conversations.values()) {
|
|
@@ -157,6 +223,16 @@ export class Conversations {
|
|
|
157
223
|
}
|
|
158
224
|
opened() {
|
|
159
225
|
this.watched = false;
|
|
226
|
+
for (const conversation of this.conversations.values()) {
|
|
227
|
+
if (conversation.running !== null) {
|
|
228
|
+
this.facts(conversation);
|
|
229
|
+
this.subagentsChanged(conversation);
|
|
230
|
+
}
|
|
231
|
+
else if (conversation.exitUntold)
|
|
232
|
+
this.process(conversation, false);
|
|
233
|
+
if (conversation.untold || conversation.told !== null)
|
|
234
|
+
this.retell(conversation);
|
|
235
|
+
}
|
|
160
236
|
}
|
|
161
237
|
watching(watched) {
|
|
162
238
|
const resend = watched && !this.watched;
|
|
@@ -176,130 +252,320 @@ export class Conversations {
|
|
|
176
252
|
}
|
|
177
253
|
return conversation;
|
|
178
254
|
}
|
|
179
|
-
async carry(conversation,
|
|
180
|
-
if (
|
|
181
|
-
return
|
|
182
|
-
if (
|
|
183
|
-
return this.message(conversation,
|
|
184
|
-
if (
|
|
255
|
+
async carry(conversation, call) {
|
|
256
|
+
if (call.kind === 'open')
|
|
257
|
+
return this.open(conversation, call);
|
|
258
|
+
if (call.kind === 'message')
|
|
259
|
+
return this.message(conversation, call);
|
|
260
|
+
if (call.kind === 'done') {
|
|
261
|
+
conversation.done = true;
|
|
262
|
+
this.next(conversation);
|
|
263
|
+
return { result: Promise.resolve({}) };
|
|
264
|
+
}
|
|
265
|
+
await this.launched(conversation);
|
|
266
|
+
if (call.kind === 'interrupt') {
|
|
185
267
|
if (--conversation.interrupts === 0)
|
|
186
268
|
conversation.interrupted = new AbortController();
|
|
187
|
-
await this.interrupt(conversation, String(
|
|
269
|
+
await this.interrupt(conversation, String(call.turn_id));
|
|
188
270
|
}
|
|
189
|
-
if (
|
|
271
|
+
if (call.kind === 'kill') {
|
|
190
272
|
await this.kill(conversation);
|
|
191
273
|
await conversation.sending;
|
|
192
274
|
if (--conversation.kills === 0)
|
|
193
275
|
conversation.killed = new AbortController();
|
|
194
276
|
}
|
|
195
|
-
if (
|
|
196
|
-
await this.option(conversation, String(
|
|
197
|
-
if (
|
|
198
|
-
const question = this.questions.get(String(
|
|
277
|
+
if (call.kind === 'option')
|
|
278
|
+
await this.option(conversation, String(call.option), call.value);
|
|
279
|
+
if (call.kind === 'answer') {
|
|
280
|
+
const question = this.questions.get(String(call.interaction_id));
|
|
199
281
|
if (question === undefined) {
|
|
200
|
-
const text = answerMessage(
|
|
201
|
-
return this.message(conversation, { ...
|
|
282
|
+
const text = answerMessage(call.request, call.response);
|
|
283
|
+
return this.message(conversation, { ...call, text, first: false, message_id: null });
|
|
202
284
|
}
|
|
203
|
-
question.answer(
|
|
285
|
+
question.answer(call.response);
|
|
204
286
|
}
|
|
205
|
-
return {
|
|
287
|
+
return { result: Promise.resolve({}) };
|
|
288
|
+
}
|
|
289
|
+
async open(conversation, call) {
|
|
290
|
+
await this.stop(conversation);
|
|
291
|
+
const launch = conversation.launch ?? this.launch(conversation, this.route(conversation, call.agent_id), null, true);
|
|
292
|
+
return {
|
|
293
|
+
result: launch.session.then((session) => ({ provider_session_id: session }), (error) => ({ refused: refusal(conversation, error) })),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
async stop(conversation) {
|
|
297
|
+
const running = conversation.running;
|
|
298
|
+
running?.scope.kill();
|
|
299
|
+
await running?.adapter.exited;
|
|
300
|
+
}
|
|
301
|
+
launch(conversation, route, sessionId, empty) {
|
|
302
|
+
let opened;
|
|
303
|
+
let failed;
|
|
304
|
+
const bridge = new Promise((resolve, reject) => {
|
|
305
|
+
opened = resolve;
|
|
306
|
+
failed = reject;
|
|
307
|
+
});
|
|
308
|
+
bridge.catch(() => undefined);
|
|
309
|
+
const session = this.start(conversation, route, sessionId, empty, opened);
|
|
310
|
+
session.catch(failed);
|
|
311
|
+
const launch = { bridge, session };
|
|
312
|
+
conversation.launch = launch;
|
|
313
|
+
void session
|
|
314
|
+
.catch(() => undefined)
|
|
315
|
+
.then(() => {
|
|
316
|
+
if (conversation.launch !== launch)
|
|
317
|
+
return;
|
|
318
|
+
conversation.launch = null;
|
|
319
|
+
this.closeIdleBridge(conversation);
|
|
320
|
+
this.next(conversation);
|
|
321
|
+
});
|
|
322
|
+
this.next(conversation);
|
|
323
|
+
return launch;
|
|
324
|
+
}
|
|
325
|
+
async launched(conversation) {
|
|
326
|
+
while (conversation.launch !== null)
|
|
327
|
+
await conversation.launch.session.catch(() => undefined);
|
|
206
328
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
329
|
+
instructed(conversation, route) {
|
|
330
|
+
const bridge = conversation.running === null ? conversation.launch?.bridge : Promise.resolve(conversation.running.bridge);
|
|
331
|
+
if (bridge === undefined)
|
|
332
|
+
return null;
|
|
333
|
+
const read = Promise.all([bridge, this.kit.copies.mapping()]).then(([opened, copies]) => instructions(opened, route.base_instructions, copies));
|
|
334
|
+
read.catch(() => undefined);
|
|
335
|
+
return read;
|
|
336
|
+
}
|
|
337
|
+
async message(conversation, call) {
|
|
338
|
+
const content = [
|
|
339
|
+
...(typeof call.recent === 'string' ? [{ type: 'text', text: call.recent }] : []),
|
|
340
|
+
{ type: 'text', text: String(call.text) },
|
|
341
|
+
];
|
|
342
|
+
let route;
|
|
343
|
+
let instructing = null;
|
|
210
344
|
try {
|
|
211
|
-
|
|
345
|
+
route = await this.route(conversation, call.agent_id);
|
|
346
|
+
if (call.first === true)
|
|
347
|
+
instructing = this.instructed(conversation, route);
|
|
348
|
+
if (call.message_id !== null)
|
|
349
|
+
content.push(...(await this.transcribed(conversation, String(call.message_id), String(call.text))));
|
|
350
|
+
if (conversation.kills > 0)
|
|
351
|
+
throw new Error(KILLED);
|
|
212
352
|
}
|
|
213
353
|
catch (error) {
|
|
214
|
-
|
|
354
|
+
this.closeIdleBridge(conversation);
|
|
355
|
+
this.prepares(conversation, null);
|
|
356
|
+
return { result: Promise.resolve({ refused: refusal(conversation, error) }) };
|
|
215
357
|
}
|
|
358
|
+
conversation.preparing = null;
|
|
359
|
+
const { promise, resolve: settle } = Promise.withResolvers();
|
|
360
|
+
const waiting = { call, route, content, instructing, settle };
|
|
361
|
+
if (call.at_once === true && conversation.turns.size > 0)
|
|
362
|
+
await this.atOnce(conversation, waiting);
|
|
363
|
+
else
|
|
364
|
+
conversation.waiting.push(waiting);
|
|
365
|
+
this.next(conversation);
|
|
366
|
+
return { result: promise };
|
|
216
367
|
}
|
|
217
|
-
async
|
|
218
|
-
|
|
219
|
-
|
|
368
|
+
async atOnce(conversation, waiting) {
|
|
369
|
+
while (conversation.sending !== null)
|
|
370
|
+
await conversation.sending;
|
|
371
|
+
const running = conversation.running;
|
|
372
|
+
if (running !== null && conversation.turns.size > 0 && steers(running.adapter)) {
|
|
373
|
+
conversation.sending = this.steer(conversation, running, waiting);
|
|
374
|
+
await conversation.sending;
|
|
375
|
+
conversation.sending = null;
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
this.ahead(conversation, waiting);
|
|
379
|
+
if (conversation.turns.size > 0)
|
|
380
|
+
await this.cancel(conversation);
|
|
381
|
+
}
|
|
382
|
+
ahead(conversation, waiting) {
|
|
383
|
+
const behind = conversation.waiting.findIndex((held) => held.call.at_once !== true);
|
|
384
|
+
conversation.waiting.splice(behind === -1 ? conversation.waiting.length : behind, 0, waiting);
|
|
385
|
+
}
|
|
386
|
+
async steer(conversation, running, waiting) {
|
|
387
|
+
let outcome;
|
|
220
388
|
try {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
content.push({ type: 'text', text: lines.join('\n') });
|
|
227
|
-
if (conversation.kills > 0)
|
|
228
|
-
throw new Error(KILLED);
|
|
389
|
+
({ outcome } = await running.adapter.connection.agent.request('_session/steering', {
|
|
390
|
+
sessionId: running.sessionId,
|
|
391
|
+
prompt: waiting.content,
|
|
392
|
+
_meta: { steering: { idleBehavior: 'promptRequired' } },
|
|
393
|
+
}));
|
|
229
394
|
}
|
|
230
395
|
catch (error) {
|
|
231
|
-
|
|
396
|
+
outcome = error;
|
|
397
|
+
}
|
|
398
|
+
if (outcome === 'promptRequired') {
|
|
399
|
+
this.ahead(conversation, waiting);
|
|
400
|
+
}
|
|
401
|
+
else if (outcome === 'injected' || outcome === 'startedNewTurn') {
|
|
402
|
+
if (outcome === 'injected') {
|
|
403
|
+
for (const turn of conversation.turns)
|
|
404
|
+
turn.steered ||= turn.prompted;
|
|
405
|
+
}
|
|
406
|
+
this.kit.calls.keepResult(String(waiting.call.input_id), {});
|
|
407
|
+
waiting.settle({});
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const cause = outcome instanceof Error ? outcome : new Error(`the CLI answered ${JSON.stringify(outcome)}`);
|
|
411
|
+
waiting.settle({ refused: refusal(conversation, new NotStarted(UNSTEERED, { cause })) });
|
|
232
412
|
}
|
|
233
|
-
return {
|
|
234
|
-
ack: new Promise((settle) => {
|
|
235
|
-
conversation.waiting.push({ input, route, content, settle });
|
|
236
|
-
this.next(conversation);
|
|
237
|
-
}),
|
|
238
|
-
};
|
|
239
413
|
}
|
|
240
414
|
next(conversation) {
|
|
241
|
-
if (conversation.sending !== null || conversation.
|
|
415
|
+
if (conversation.sending !== null || conversation.turns.size > 0)
|
|
242
416
|
return;
|
|
243
417
|
const waiting = conversation.waiting.shift();
|
|
244
|
-
if (waiting === undefined)
|
|
418
|
+
if (waiting === undefined) {
|
|
419
|
+
const step = conversation.preparing ?? (conversation.launch === null ? null : 'session');
|
|
420
|
+
if (conversation.starting !== step)
|
|
421
|
+
this.starts(conversation, step);
|
|
422
|
+
if (conversation.done && conversation.launch === null)
|
|
423
|
+
void this.stop(conversation).catch(logged);
|
|
245
424
|
return;
|
|
425
|
+
}
|
|
246
426
|
conversation.sending = this.send(conversation, waiting).finally(() => {
|
|
247
427
|
conversation.sending = null;
|
|
248
428
|
this.next(conversation);
|
|
249
429
|
});
|
|
250
430
|
}
|
|
251
431
|
async send(conversation, waiting) {
|
|
252
|
-
const {
|
|
253
|
-
let
|
|
432
|
+
const { call } = waiting;
|
|
433
|
+
let result = {};
|
|
434
|
+
let started = false;
|
|
435
|
+
let instructing = waiting.instructing;
|
|
254
436
|
const prompt = [];
|
|
255
437
|
try {
|
|
256
438
|
if (conversation.running === null) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (resumed === null)
|
|
260
|
-
ack = { provider_session_id: opened };
|
|
439
|
+
this.starts(conversation, 'session');
|
|
440
|
+
await this.launched(conversation);
|
|
261
441
|
}
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
442
|
+
if (conversation.running === null) {
|
|
443
|
+
const stored = conversation.session ?? call.provider_session_id;
|
|
444
|
+
const empty = call.first === true && conversation.prompted !== stored;
|
|
445
|
+
const launch = this.launch(conversation, Promise.resolve(waiting.route), stored, empty);
|
|
446
|
+
if (call.first === true)
|
|
447
|
+
instructing = this.instructed(conversation, waiting.route);
|
|
448
|
+
started = (await launch.session) !== stored;
|
|
449
|
+
}
|
|
450
|
+
const session = conversation.running.sessionId;
|
|
451
|
+
if (started || (call.first === true && session !== call.provider_session_id)) {
|
|
452
|
+
result = { provider_session_id: session };
|
|
453
|
+
}
|
|
454
|
+
if (call.first === true) {
|
|
455
|
+
prompt.push({ type: 'text', text: await (instructing ?? this.instructed(conversation, waiting.route)) });
|
|
267
456
|
}
|
|
268
457
|
if (conversation.kills > 0)
|
|
269
458
|
throw new Error(KILLED);
|
|
270
459
|
}
|
|
271
460
|
catch (error) {
|
|
272
|
-
|
|
461
|
+
this.starts(conversation, null);
|
|
462
|
+
if (started) {
|
|
273
463
|
conversation.session = null;
|
|
274
464
|
await this.kill(conversation);
|
|
275
465
|
}
|
|
276
|
-
|
|
466
|
+
const refused = refusal(conversation, error);
|
|
467
|
+
const resumeRefused = call.kind === 'message' && conversation.kills === 0 && error instanceof ResumeRefused;
|
|
468
|
+
waiting.settle(resumeRefused ? { refused, resume_refused: true } : { refused });
|
|
277
469
|
return;
|
|
278
470
|
}
|
|
279
|
-
this.
|
|
280
|
-
waiting.
|
|
471
|
+
this.starts(conversation, null);
|
|
472
|
+
this.prompt(conversation, [...prompt, ...waiting.content], () => this.kit.calls.keepResult(String(call.input_id), result));
|
|
473
|
+
waiting.settle(result);
|
|
281
474
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
475
|
+
starts(conversation, step) {
|
|
476
|
+
conversation.starting = step;
|
|
477
|
+
if (conversation.shown !== null)
|
|
478
|
+
clearTimeout(conversation.shown);
|
|
479
|
+
conversation.shown = null;
|
|
480
|
+
if (step === null || conversation.told !== null) {
|
|
481
|
+
this.tell(conversation, step);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
conversation.shown = setTimeout(() => {
|
|
485
|
+
conversation.shown = null;
|
|
486
|
+
if (conversation.starting === step)
|
|
487
|
+
this.tell(conversation, step);
|
|
488
|
+
}, STARTING_SHOWN_AFTER_MS);
|
|
489
|
+
}
|
|
490
|
+
prepares(conversation, step) {
|
|
491
|
+
conversation.preparing = step;
|
|
492
|
+
this.next(conversation);
|
|
493
|
+
}
|
|
494
|
+
tell(conversation, step) {
|
|
495
|
+
if (conversation.told === step && !conversation.untold)
|
|
496
|
+
return;
|
|
497
|
+
conversation.told = step;
|
|
498
|
+
this.retell(conversation);
|
|
499
|
+
}
|
|
500
|
+
retell(conversation) {
|
|
501
|
+
conversation.untold = !this.kit.send({ type: 'starting', conversation_id: conversation.id, step: conversation.told });
|
|
502
|
+
}
|
|
503
|
+
async transcribed(conversation, messageId, text) {
|
|
504
|
+
const paths = linkedPaths(text);
|
|
505
|
+
if (paths.length === 0)
|
|
506
|
+
return [];
|
|
507
|
+
const killed = conversation.killed.signal;
|
|
508
|
+
const interrupted = conversation.interrupted.signal;
|
|
509
|
+
const transcribing = AbortSignal.any([killed, interrupted]);
|
|
510
|
+
let signal = killed;
|
|
511
|
+
const blocks = [];
|
|
512
|
+
try {
|
|
513
|
+
const bridge = await step(unbridged, unlessAborted(this.bridged(conversation), killed));
|
|
514
|
+
const read = async (path) => audioOf(await step(unbridged, unlessAborted(bridge.tool('inspect', { path }), signal)));
|
|
515
|
+
for (const path of paths) {
|
|
516
|
+
let answered = await read(path);
|
|
517
|
+
if (answered === null)
|
|
518
|
+
continue;
|
|
519
|
+
signal = transcribing;
|
|
520
|
+
this.prepares(conversation, 'transcription');
|
|
521
|
+
const said = await transcribe(async () => {
|
|
522
|
+
const address = answered ?? (await read(path));
|
|
523
|
+
answered = null;
|
|
524
|
+
if (address === null)
|
|
525
|
+
throw new Error(`${path} is no longer an audio file`);
|
|
526
|
+
return address;
|
|
527
|
+
}, signal);
|
|
528
|
+
const transcript = `/kit/conversations/${conversation.id}/messages/${messageId}/transcripts`;
|
|
529
|
+
await step(unbridged, unlessAborted(this.kit.house.deliver(transcript, { path, text: said }, () => !signal.aborted), signal));
|
|
530
|
+
blocks.push({ type: 'text', text: `${path}\nTranscript: ${said}` });
|
|
297
531
|
}
|
|
298
532
|
}
|
|
299
|
-
|
|
533
|
+
catch (error) {
|
|
534
|
+
if (signal === transcribing && interrupted.aborted && conversation.kills === 0)
|
|
535
|
+
throw new NotStarted(INTERRUPTED, { cause: error });
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
return blocks;
|
|
539
|
+
}
|
|
540
|
+
bridged(conversation) {
|
|
541
|
+
if (conversation.bridge === null) {
|
|
542
|
+
const bridging = openBridge(this.kit.house, this.kit.copies, conversation.id, conversation.killed.signal);
|
|
543
|
+
conversation.bridge = bridging;
|
|
544
|
+
bridging.catch(() => {
|
|
545
|
+
if (conversation.bridge === bridging)
|
|
546
|
+
conversation.bridge = null;
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
return conversation.bridge;
|
|
550
|
+
}
|
|
551
|
+
closeBridge(conversation, bridging, bridge) {
|
|
552
|
+
bridge.close();
|
|
553
|
+
if (conversation.bridge === bridging)
|
|
554
|
+
conversation.bridge = null;
|
|
555
|
+
}
|
|
556
|
+
closeIdleBridge(conversation) {
|
|
557
|
+
const bridging = conversation.bridge;
|
|
558
|
+
if (bridging === null || conversation.running !== null || conversation.launch !== null)
|
|
559
|
+
return;
|
|
560
|
+
conversation.bridge = null;
|
|
561
|
+
void bridging.then((bridge) => bridge.close(), () => undefined);
|
|
300
562
|
}
|
|
301
563
|
async interrupt(conversation, turnId) {
|
|
302
|
-
if (conversation.
|
|
564
|
+
if ([...conversation.turns].some((turn) => turn.id === turnId))
|
|
565
|
+
await this.cancel(conversation);
|
|
566
|
+
}
|
|
567
|
+
async cancel(conversation) {
|
|
568
|
+
if (conversation.running === null)
|
|
303
569
|
return;
|
|
304
570
|
await conversation.running.adapter.connection.agent.notify('session/cancel', {
|
|
305
571
|
sessionId: conversation.running.sessionId,
|
|
@@ -351,7 +617,8 @@ export class Conversations {
|
|
|
351
617
|
}
|
|
352
618
|
return route;
|
|
353
619
|
}
|
|
354
|
-
async start(conversation,
|
|
620
|
+
async start(conversation, routed, sessionId, empty, bridged) {
|
|
621
|
+
const route = await routed;
|
|
355
622
|
const reader = readerFor(route.kind);
|
|
356
623
|
reader.session = sessionId;
|
|
357
624
|
const app = client({ name: '@agentshouse/kit' })
|
|
@@ -359,56 +626,69 @@ export class Conversations {
|
|
|
359
626
|
.onRequest('elicitation/create', ({ params, signal }) => this.ask(conversation, 'elicitation/create', params, asksSecret(params), signal))
|
|
360
627
|
.onRequest('_x.ai/exit_plan_mode', EXIT_PLAN_MODE, ({ params, signal }) => this.ask(conversation, '_x.ai/exit_plan_mode', params, false, signal));
|
|
361
628
|
const cli = await unlessAborted(this.kit.agents.cli(route.kind), conversation.killed.signal);
|
|
362
|
-
const
|
|
629
|
+
const bridging = this.bridged(conversation);
|
|
630
|
+
const bridge = await step(unbridged, bridging);
|
|
363
631
|
if (conversation.kills > 0) {
|
|
364
|
-
|
|
632
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
365
633
|
throw new Error(KILLED);
|
|
366
634
|
}
|
|
367
635
|
conversation.opening = bridge;
|
|
368
636
|
const work = workFor(route.kind, route.working_directory, () => this.kit.changed());
|
|
369
637
|
const scope = new Scope(marker(bridge), randomUUID(), cli);
|
|
638
|
+
let adapter = null;
|
|
370
639
|
try {
|
|
371
|
-
|
|
640
|
+
bridged(bridge);
|
|
372
641
|
await CLIS[route.kind].allowHouse(bridge.env.PATH);
|
|
373
|
-
const
|
|
642
|
+
const streamed = await step(NO_CONNECTION, work.open(cli));
|
|
643
|
+
adapter = await step(NO_START, startAdapter(route.kind, cli, route.working_directory, app, (message) => {
|
|
374
644
|
work.read(message);
|
|
375
645
|
this.read(conversation, reader, message);
|
|
376
|
-
}, { ...bridge.env, ...
|
|
377
|
-
void adapter.exited.then(() =>
|
|
646
|
+
}, { ...bridge.env, ...streamed }, undefined, scope));
|
|
647
|
+
void adapter.exited.then(() => this.closeBridge(conversation, bridging, bridge));
|
|
378
648
|
const agent = adapter.connection.agent;
|
|
379
649
|
const cwd = route.working_directory;
|
|
380
650
|
const _meta = CLIS[route.kind].sessionMeta(bridge.env.HOUSE_BRIDGE, conversation.id);
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
: { ...(await agent.request('session/resume', { sessionId, cwd, mcpServers: [], _meta })), sessionId };
|
|
651
|
+
const resumed = sessionId === null ? null : await this.resume(conversation, route.kind, adapter, { sessionId, cwd, mcpServers: [], _meta }, empty);
|
|
652
|
+
const opened = resumed ?? (await step(unopened(NO_SESSION), agent.request('session/new', { cwd, mcpServers: [], _meta })));
|
|
384
653
|
reader.session = opened.sessionId;
|
|
385
|
-
if (sessionId
|
|
654
|
+
if (opened.sessionId !== sessionId && CLIS[route.kind].renames) {
|
|
386
655
|
const prompt = [{ type: 'text', text: `/rename ${conversation.id}` }];
|
|
387
|
-
await agent.request('session/prompt', { sessionId: opened.sessionId, prompt });
|
|
656
|
+
await step(unopened(NO_SESSION), agent.request('session/prompt', { sessionId: opened.sessionId, prompt }));
|
|
388
657
|
}
|
|
389
|
-
const running = { adapter, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
390
|
-
const options = await this.launchSettings(running, route, opened.configOptions ?? []);
|
|
658
|
+
const running = { adapter, bridging, bridge, sessionId: opened.sessionId, killed: false, reader, work, scope };
|
|
659
|
+
const options = await step(NO_SETTINGS, this.launchSettings(running, route, opened.configOptions ?? []));
|
|
391
660
|
void work.settled().then(() => scope.record());
|
|
392
661
|
conversation.running = running;
|
|
662
|
+
adapter.turn(conversation.turns.size > 0);
|
|
393
663
|
conversation.session = running.sessionId;
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
this.options(conversation, options);
|
|
398
|
-
void adapter.exited.then((cause) => this.exited(conversation, adapter, cause));
|
|
664
|
+
conversation.options = options;
|
|
665
|
+
this.facts(conversation);
|
|
666
|
+
void adapter.lost.then((cause) => this.exited(conversation, running, cause));
|
|
399
667
|
return running.sessionId;
|
|
400
668
|
}
|
|
401
669
|
catch (error) {
|
|
402
|
-
|
|
670
|
+
adapter?.retire();
|
|
403
671
|
work.close();
|
|
404
672
|
conversation.leftovers.add(scope);
|
|
405
|
-
|
|
673
|
+
this.closeBridge(conversation, bridging, bridge);
|
|
406
674
|
throw conversation.kills > 0 ? new Error(KILLED) : error;
|
|
407
675
|
}
|
|
408
676
|
finally {
|
|
409
677
|
conversation.opening = null;
|
|
410
678
|
}
|
|
411
679
|
}
|
|
680
|
+
async resume(conversation, kind, adapter, request, empty) {
|
|
681
|
+
try {
|
|
682
|
+
return { ...(await adapter.connection.agent.request('session/resume', request)), sessionId: request.sessionId };
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
if (!empty || conversation.kills > 0 || !CLIS[kind].unknownSession(error)) {
|
|
686
|
+
throw signedOut(error) ? new NotStarted(NOT_SIGNED_IN, { cause: error }) : new ResumeRefused(NO_RESUME, { cause: error });
|
|
687
|
+
}
|
|
688
|
+
logged(new Error(`conversation ${conversation.id} opens a new session: ${NO_RESUME}: ${causeOf(error)}`));
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
412
692
|
async launchSettings(running, route, offered) {
|
|
413
693
|
let options = offered;
|
|
414
694
|
const settings = [
|
|
@@ -430,27 +710,45 @@ export class Conversations {
|
|
|
430
710
|
}
|
|
431
711
|
return options;
|
|
432
712
|
}
|
|
433
|
-
exited(conversation,
|
|
434
|
-
const running = conversation.running;
|
|
435
|
-
if (running?.adapter !== adapter)
|
|
436
|
-
return;
|
|
713
|
+
exited(conversation, running, cause) {
|
|
437
714
|
this.detach(conversation, running);
|
|
438
715
|
if (conversation.turn !== null) {
|
|
439
|
-
this.end(conversation, conversation.turn,
|
|
716
|
+
this.end(conversation, conversation.turn, exitedWith(running, cause));
|
|
440
717
|
}
|
|
441
718
|
this.next(conversation);
|
|
442
719
|
this.kit.changed();
|
|
443
720
|
}
|
|
444
721
|
detach(conversation, running) {
|
|
722
|
+
if (conversation.bridge === running.bridging)
|
|
723
|
+
conversation.bridge = null;
|
|
445
724
|
running.work.close();
|
|
446
725
|
conversation.leftovers.add(running.scope);
|
|
447
726
|
conversation.running = null;
|
|
448
727
|
conversation.commands = null;
|
|
728
|
+
conversation.options = null;
|
|
729
|
+
if (conversation.subagents.size > 0) {
|
|
730
|
+
conversation.subagents.clear();
|
|
731
|
+
this.subagentsChanged(conversation);
|
|
732
|
+
}
|
|
449
733
|
for (const [id, question] of this.questions) {
|
|
450
734
|
if (question.conversation === conversation)
|
|
451
735
|
this.questions.delete(id);
|
|
452
736
|
}
|
|
453
|
-
this.
|
|
737
|
+
this.process(conversation, false);
|
|
738
|
+
}
|
|
739
|
+
process(conversation, running) {
|
|
740
|
+
conversation.exitUntold = !this.kit.send({ type: 'process', conversation_id: conversation.id, running }) && !running;
|
|
741
|
+
}
|
|
742
|
+
facts(conversation) {
|
|
743
|
+
this.process(conversation, true);
|
|
744
|
+
if (conversation.commands !== null)
|
|
745
|
+
this.commands(conversation, conversation.commands);
|
|
746
|
+
if (conversation.options !== null)
|
|
747
|
+
this.options(conversation, conversation.options);
|
|
748
|
+
}
|
|
749
|
+
subagentsChanged(conversation) {
|
|
750
|
+
const subagents = [...conversation.subagents.values()].map(({ description, activity }) => ({ description, activity }));
|
|
751
|
+
this.kit.send({ type: 'subagents', conversation_id: conversation.id, subagents });
|
|
454
752
|
}
|
|
455
753
|
commands(conversation, commands) {
|
|
456
754
|
conversation.commands = commands;
|
|
@@ -461,6 +759,7 @@ export class Conversations {
|
|
|
461
759
|
options(conversation, options) {
|
|
462
760
|
if (conversation.running === null)
|
|
463
761
|
return;
|
|
762
|
+
conversation.options = options;
|
|
464
763
|
this.kit.send({ type: 'options', conversation_id: conversation.id, options });
|
|
465
764
|
}
|
|
466
765
|
deliver(conversation, turn, event, body) {
|
|
@@ -475,7 +774,7 @@ export class Conversations {
|
|
|
475
774
|
return delivered;
|
|
476
775
|
}
|
|
477
776
|
write(conversation, turn, operation) {
|
|
478
|
-
if (
|
|
777
|
+
if (turn.ended || !this.watched)
|
|
479
778
|
return;
|
|
480
779
|
this.kit.send({ type: 'turn', conversation_id: conversation.id, turn_id: turn.id, ...operation });
|
|
481
780
|
}
|
|
@@ -485,13 +784,9 @@ export class Conversations {
|
|
|
485
784
|
}
|
|
486
785
|
begin(conversation) {
|
|
487
786
|
const turn = new Turn((operation) => this.write(conversation, turn, operation));
|
|
488
|
-
conversation.
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
turn.admitted = true;
|
|
492
|
-
this.replay(conversation, turn);
|
|
493
|
-
})
|
|
494
|
-
.catch(logged);
|
|
787
|
+
conversation.turns.add(turn);
|
|
788
|
+
conversation.running?.adapter.turn(true);
|
|
789
|
+
void this.deliver(conversation, turn, 'started', {}).catch(logged);
|
|
495
790
|
conversation.turn = turn;
|
|
496
791
|
if (conversation.context !== null)
|
|
497
792
|
turn.parts.measure(conversation.context);
|
|
@@ -500,59 +795,81 @@ export class Conversations {
|
|
|
500
795
|
this.kit.changed();
|
|
501
796
|
return turn;
|
|
502
797
|
}
|
|
503
|
-
end(conversation, turn, failed) {
|
|
798
|
+
end(conversation, turn, failed, notice = null) {
|
|
504
799
|
if (turn.ended)
|
|
505
800
|
return;
|
|
801
|
+
if (failed !== null)
|
|
802
|
+
logged(new Error(`conversation ${conversation.id} turn ${turn.id} failed: ${failed.cause}`));
|
|
506
803
|
if (conversation.turn === turn) {
|
|
507
804
|
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
508
805
|
this.apply(conversation, turn, event);
|
|
509
806
|
}
|
|
807
|
+
if (notice !== null)
|
|
808
|
+
turn.parts.start({ type: 'marker', marker: 'retry', text: notice });
|
|
510
809
|
turn.ended = true;
|
|
511
|
-
conversation.
|
|
810
|
+
conversation.turns.delete(turn);
|
|
811
|
+
conversation.running?.adapter.turn(conversation.turns.size > 0);
|
|
512
812
|
if (conversation.turn === turn)
|
|
513
813
|
conversation.turn = null;
|
|
514
814
|
const body = Promise.allSettled(turn.uploads).then(() => ({
|
|
515
815
|
parts: turn.parts.saved(),
|
|
516
816
|
context: turn.parts.context,
|
|
517
|
-
...failed,
|
|
817
|
+
...(failed === null ? {} : { failed: failed.failure }),
|
|
518
818
|
}));
|
|
519
819
|
void this.deliver(conversation, turn, 'ended', body).catch(logged);
|
|
520
820
|
this.next(conversation);
|
|
521
821
|
this.kit.changed();
|
|
522
822
|
}
|
|
523
|
-
prompt(conversation, prompt) {
|
|
823
|
+
prompt(conversation, prompt, taken) {
|
|
524
824
|
const running = conversation.running;
|
|
525
825
|
const turn = this.begin(conversation);
|
|
526
826
|
turn.prompted = true;
|
|
527
|
-
|
|
827
|
+
turn.taken = taken;
|
|
828
|
+
conversation.prompted = running.sessionId;
|
|
829
|
+
running.adapter.connection.agent.request('session/prompt', { sessionId: running.sessionId, prompt }).then((answer) => {
|
|
830
|
+
this.took(turn);
|
|
831
|
+
const notice = answeredFailure(answer);
|
|
832
|
+
if (notice !== null) {
|
|
833
|
+
this.end(conversation, turn, { failure: notice.limit ? 'limit' : 'error', cause: notice.title }, notice.title);
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
this.end(conversation, turn, answer.stopReason === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
837
|
+
}
|
|
838
|
+
}, async (error) => {
|
|
528
839
|
if (running.adapter.connection.signal.aborted) {
|
|
529
|
-
this.end(conversation, turn,
|
|
840
|
+
this.end(conversation, turn, exitedWith(running, await running.adapter.exited));
|
|
530
841
|
}
|
|
531
842
|
else {
|
|
532
|
-
|
|
533
|
-
|
|
843
|
+
this.took(turn);
|
|
844
|
+
if (conversation.turns.size === 1 && conversation.turns.has(turn) && conversation.running === running) {
|
|
845
|
+
running.adapter.retire();
|
|
534
846
|
this.detach(conversation, running);
|
|
535
847
|
}
|
|
536
|
-
this.end(conversation, turn,
|
|
848
|
+
this.end(conversation, turn, answeredWith(error));
|
|
537
849
|
}
|
|
538
850
|
});
|
|
539
851
|
}
|
|
540
852
|
started(conversation) {
|
|
541
|
-
|
|
853
|
+
const turn = conversation.turn ?? this.begin(conversation);
|
|
854
|
+
this.took(turn);
|
|
855
|
+
return turn;
|
|
542
856
|
}
|
|
543
|
-
ended(conversation) {
|
|
857
|
+
ended(conversation, stop) {
|
|
544
858
|
const turn = conversation.turn;
|
|
545
859
|
if (turn === null)
|
|
546
860
|
return;
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
this.apply(conversation, turn, event);
|
|
553
|
-
conversation.turn = null;
|
|
554
|
-
this.kit.changed();
|
|
861
|
+
const held = stop !== null && !turn.steered && [...conversation.subagents.values()].some((subagent) => subagent.turn === turn);
|
|
862
|
+
if (!turn.prompted || held) {
|
|
863
|
+
this.took(turn);
|
|
864
|
+
this.end(conversation, turn, stop === 'refusal' ? { failure: 'refused', cause: REFUSED } : null);
|
|
865
|
+
return;
|
|
555
866
|
}
|
|
867
|
+
if (stop !== null)
|
|
868
|
+
return;
|
|
869
|
+
for (const event of conversation.running?.reader.closeTurn() ?? [])
|
|
870
|
+
this.apply(conversation, turn, event);
|
|
871
|
+
conversation.turn = null;
|
|
872
|
+
this.kit.changed();
|
|
556
873
|
}
|
|
557
874
|
async ask(conversation, method, params, secret, signal) {
|
|
558
875
|
const turn = this.started(conversation);
|
|
@@ -608,7 +925,7 @@ export class Conversations {
|
|
|
608
925
|
this.started(conversation);
|
|
609
926
|
return;
|
|
610
927
|
case 'ended':
|
|
611
|
-
this.ended(conversation);
|
|
928
|
+
this.ended(conversation, event.stop);
|
|
612
929
|
return;
|
|
613
930
|
case 'commands':
|
|
614
931
|
this.commands(conversation, event.commands);
|
|
@@ -623,6 +940,9 @@ export class Conversations {
|
|
|
623
940
|
case 'subagent-done':
|
|
624
941
|
this.subagentDone(conversation, event.id, event.result);
|
|
625
942
|
return;
|
|
943
|
+
case 'subagent-activity':
|
|
944
|
+
this.subagentActivity(conversation, event.id, event.activity);
|
|
945
|
+
return;
|
|
626
946
|
case 'marker':
|
|
627
947
|
case 'image':
|
|
628
948
|
if (conversation.turn !== null)
|
|
@@ -633,6 +953,11 @@ export class Conversations {
|
|
|
633
953
|
}
|
|
634
954
|
this.apply(conversation, this.started(conversation), event);
|
|
635
955
|
}
|
|
956
|
+
took(turn) {
|
|
957
|
+
const taken = turn.taken;
|
|
958
|
+
turn.taken = null;
|
|
959
|
+
taken?.();
|
|
960
|
+
}
|
|
636
961
|
apply(conversation, turn, event) {
|
|
637
962
|
switch (event.kind) {
|
|
638
963
|
case 'text':
|
|
@@ -644,7 +969,8 @@ export class Conversations {
|
|
|
644
969
|
return;
|
|
645
970
|
case 'subagent': {
|
|
646
971
|
const index = turn.parts.start({ type: 'subagent', description: event.description, status: 'running', result: null });
|
|
647
|
-
conversation.subagents.set(event.id, { description: event.description, turn, index });
|
|
972
|
+
conversation.subagents.set(event.id, { description: event.description, turn, index, activity: null });
|
|
973
|
+
this.subagentsChanged(conversation);
|
|
648
974
|
return;
|
|
649
975
|
}
|
|
650
976
|
case 'message':
|
|
@@ -667,6 +993,7 @@ export class Conversations {
|
|
|
667
993
|
if (subagent === undefined)
|
|
668
994
|
return;
|
|
669
995
|
conversation.subagents.delete(id);
|
|
996
|
+
this.subagentsChanged(conversation);
|
|
670
997
|
if (subagent.turn === conversation.turn) {
|
|
671
998
|
subagent.turn.parts.done(subagent.index, result);
|
|
672
999
|
return;
|
|
@@ -677,23 +1004,37 @@ export class Conversations {
|
|
|
677
1004
|
else
|
|
678
1005
|
conversation.turn.parts.start(part);
|
|
679
1006
|
}
|
|
1007
|
+
subagentActivity(conversation, id, activity) {
|
|
1008
|
+
const subagent = conversation.subagents.get(id);
|
|
1009
|
+
if (subagent === undefined || subagent.activity === activity)
|
|
1010
|
+
return;
|
|
1011
|
+
subagent.activity = activity;
|
|
1012
|
+
this.subagentsChanged(conversation);
|
|
1013
|
+
}
|
|
680
1014
|
async image(conversation, turn, reserved, data, mediaType) {
|
|
681
1015
|
const bytes = Buffer.from(data, 'base64');
|
|
682
1016
|
const name = `image-${++turn.images}.${mediaType.split('/')[1]}`;
|
|
1017
|
+
const sha256 = createHash('sha256').update(bytes).digest('hex');
|
|
683
1018
|
try {
|
|
684
1019
|
const declared = (await this.deliver(conversation, turn, 'images', {
|
|
685
1020
|
name,
|
|
686
1021
|
media_type: mediaType,
|
|
687
1022
|
bytes: bytes.length,
|
|
688
|
-
sha256
|
|
1023
|
+
sha256,
|
|
689
1024
|
}));
|
|
690
|
-
const saved =
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1025
|
+
const saved = await uploaded(declared.upload, {
|
|
1026
|
+
name,
|
|
1027
|
+
bytes: bytes.length,
|
|
1028
|
+
sha256,
|
|
1029
|
+
read: (start, end) => [bytes.subarray(start, end)],
|
|
1030
|
+
send: (url, start, end, headers) => sentPart(url, bytes.subarray(start, end), headers),
|
|
1031
|
+
finish: (upload, partSha256) => this.kit.house.deliver('/kit/uploads/finish', { upload, part_sha256: partSha256 }),
|
|
1032
|
+
});
|
|
1033
|
+
turn.parts.arrive({ type: 'image', file: saved.path, name, media_type: mediaType }, reserved);
|
|
694
1034
|
}
|
|
695
1035
|
catch (error) {
|
|
696
|
-
|
|
1036
|
+
const cause = error instanceof HouseRefusal ? (refusalOf(error.text)?.text ?? causeOf(error)) : causeOf(error);
|
|
1037
|
+
turn.parts.arrive({ type: 'marker', marker: 'retry', text: `${name} was not saved: ${cause}` }, reserved);
|
|
697
1038
|
}
|
|
698
1039
|
}
|
|
699
1040
|
}
|