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