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